From 276d9a93021ab1d4ba84498929f5870b369bbbdc Mon Sep 17 00:00:00 2001 From: "Owen W. Taylor" Date: Sat, 19 Sep 2009 20:43:49 -0400 Subject: [PATCH] Import stylesheet code from hippo-canvas Import: HippoCanvasTheme => StTheme HippoCanvasThemeImage => StThemeImage HippoCanvasStyle => StThemeNode StThemeContext is a new class managing the theme for a stage and global properties like resolution. test-theme.c is a newly written test program to do verification of the style matching and property handling rules. Various changes are made in the import: - Comprehensive reindentation - guint32 pixels replaced with ClutterColor - General pseudo-class support added - Old-fashioned (non-bordered) background image support added, though with no support for repeat, etc. - Bug fixes for problems revealed by test program https://bugzilla.gnome.org/show_bug.cgi?id=595990 --- .gitignore | 1 + configure.ac | 1 + src/Makefile-st.am | 21 +- src/Makefile.am | 5 +- src/st/st-theme-context.c | 287 ++++++ src/st/st-theme-context.h | 50 + src/st/st-theme-image.c | 92 ++ src/st/st-theme-image.h | 38 + src/st/st-theme-node.c | 1813 +++++++++++++++++++++++++++++++++++++ src/st/st-theme-node.h | 125 +++ src/st/st-theme-private.h | 22 + src/st/st-theme.c | 971 ++++++++++++++++++++ src/st/st-theme.h | 38 + src/st/test-theme.c | 292 ++++++ src/st/test-theme.css | 68 ++ 15 files changed, 3821 insertions(+), 3 deletions(-) create mode 100644 src/st/st-theme-context.c create mode 100644 src/st/st-theme-context.h create mode 100644 src/st/st-theme-image.c create mode 100644 src/st/st-theme-image.h create mode 100644 src/st/st-theme-node.c create mode 100644 src/st/st-theme-node.h create mode 100644 src/st/st-theme-private.h create mode 100644 src/st/st-theme.c create mode 100644 src/st/st-theme.h create mode 100644 src/st/test-theme.c create mode 100644 src/st/test-theme.css diff --git a/.gitignore b/.gitignore index ba962a71b..d39cc6374 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ src/gnomeshell-taskpanel src/gnome-shell src/test-recorder src/test-recorder.ogg +src/test-theme stamp-h1 tests/run-test.sh xmldocs.make diff --git a/configure.ac b/configure.ac index b1fd289c3..9a0452f64 100644 --- a/configure.ac +++ b/configure.ac @@ -60,6 +60,7 @@ PKG_CHECK_MODULES(TIDY, clutter-1.0) PKG_CHECK_MODULES(ST, clutter-1.0 gtk+-2.0 libccss-1 >= 0.3.1 clutter-imcontext-0.1) PKG_CHECK_MODULES(BIG, clutter-1.0 gtk+-2.0 librsvg-2.0) PKG_CHECK_MODULES(GDMUSER, dbus-glib-1 gtk+-2.0) +PKG_CHECK_MODULES(TOOLKIT, clutter-1.0 libcroco-0.6) PKG_CHECK_MODULES(TRAY, gtk+-2.0) MUTTER_BIN_DIR=`$PKG_CONFIG --variable=exec_prefix mutter-plugins`/bin diff --git a/src/Makefile-st.am b/src/Makefile-st.am index 2967054c9..454dce413 100644 --- a/src/Makefile-st.am +++ b/src/Makefile-st.am @@ -73,7 +73,6 @@ st_source_h = \ st/st-clipboard.h \ st/st-entry.h \ st/st-label.h \ - st/st-private.h \ st/st-stylable.h \ st/st-style.h \ st/st-scrollable.h \ @@ -82,11 +81,19 @@ st_source_h = \ st/st-subtexture.h \ st/st-texture-cache.h \ st/st-texture-frame.h \ + st/st-theme.h \ + st/st-theme-context.h \ + st/st-theme-image.h \ + st/st-theme-node.h \ st/st-tooltip.h \ st/st-types.h \ st/st-widget.h \ $(NULL) +st_source_private_h = \ + st/st-private.h \ + st/st-theme-private.h + # please, keep this sorted alphabetically st_source_c = \ st/st-adjustment.c \ @@ -106,6 +113,10 @@ st_source_c = \ st/st-subtexture.c \ st/st-texture-cache.c \ st/st-texture-frame.c \ + st/st-theme.c \ + st/st-theme-context.c \ + st/st-theme-image.c \ + st/st-theme-node.c \ st/st-tooltip.c \ st/st-widget.c \ $(NULL) @@ -115,8 +126,16 @@ noinst_LTLIBRARIES += libst-1.0.la libst_1_0_la_LIBADD = $(ST_LIBS) libst_1_0_la_SOURCES = \ $(st_source_c) \ + $(st_source_private_c) \ $(st_source_h) \ $(st_built_sources) \ $(NULL) libst_1_0_la_CPPFLAGS = $(st_cflags) libst_1_0_la_LDFLAGS = $(LDADD) + +noinst_PROGRAMS += test-theme + +test_theme_CPPFLAGS = $(st_cflags) +test_theme_LDADD = libst-1.0.la + +test_theme_SOURCES = st/test-theme.c diff --git a/src/Makefile.am b/src/Makefile.am index 04255cbb5..b747cf4a7 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -4,6 +4,7 @@ CLEANFILES = EXTRA_DIST = libexec_PROGRAMS = noinst_LTLIBRARIES = +noinst_PROGRAMS = .AUTOPARALLEL: @@ -95,7 +96,7 @@ libgnome_shell_la_SOURCES = \ shell-wm.c \ shell-wm.h -non_gir_sources = \ +non_gir_sources = \ shell-embedded-window-private.h shell_recorder_sources = \ @@ -113,7 +114,7 @@ if BUILD_RECORDER libgnome_shell_la_SOURCES += $(shell_recorder_sources) non_gir_sources += $(shell_recorder_non_gir_sources) -noinst_PROGRAMS = test-recorder +noinst_PROGRAMS += test-recorder test_recorder_CPPFLAGS = $(TEST_SHELL_RECORDER_CFLAGS) test_recorder_LDADD = $(TEST_SHELL_RECORDER_LIBS) diff --git a/src/st/st-theme-context.c b/src/st/st-theme-context.c new file mode 100644 index 000000000..d162978cf --- /dev/null +++ b/src/st/st-theme-context.c @@ -0,0 +1,287 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include + +#include "st-theme.h" +#include "st-theme-context.h" + +struct _StThemeContext { + GObject parent; + + double resolution; + PangoFontDescription *font; + StThemeNode *root_node; + StTheme *theme; +}; + +struct _StThemeContextClass { + GObjectClass parent_class; +}; + +#define DEFAULT_RESOLUTION 96. +#define DEFAULT_FONT "sans-serif 10" + +enum +{ + CHANGED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE (StThemeContext, st_theme_context, G_TYPE_OBJECT) + +static void +st_theme_context_finalize (GObject *object) +{ + StThemeContext *context = ST_THEME_CONTEXT (object); + + if (context->root_node) + g_object_unref (context->root_node); + if (context->theme) + g_object_unref (context->theme); + + pango_font_description_free (context->font); + + G_OBJECT_CLASS (st_theme_context_parent_class)->finalize (object); +} + +static void +st_theme_context_class_init (StThemeContextClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = st_theme_context_finalize; + + signals[CHANGED] = + g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, /* no default handler slot */ + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +st_theme_context_init (StThemeContext *context) +{ + context->resolution = DEFAULT_RESOLUTION; + context->font = pango_font_description_from_string (DEFAULT_FONT); +} + +/** + * st_theme_context_new: + * + * Create a new theme context not associated with any #ClutterStage. + * This can be useful in testing scenarios, or if using StThemeContext + * with something other than #ClutterActor objects, but you generally + * should use st_theme_context_get_for_stage() instead. + */ +StThemeContext * +st_theme_context_new (void) +{ + StThemeContext *context; + + context = g_object_new (ST_TYPE_THEME_CONTEXT, NULL); + + return context; +} + +static void +on_stage_destroy (ClutterStage *stage) +{ + StThemeContext *context = st_theme_context_get_for_stage (stage); + + g_object_set_data (G_OBJECT (stage), "st-theme-context", NULL); + g_object_unref (context); +} + +static void +st_theme_context_changed (StThemeContext *context) +{ + StThemeNode *old_root = context->root_node; + context->root_node = NULL; + + g_signal_emit (context, signals[CHANGED], 0); + + if (old_root) + g_object_unref (old_root); +} + +/** + * st_theme_context_get_for_stage: + * @stage: a #ClutterStage + * + * Gets a singleton theme context associated with the stage. + * + * Return value: (transfer none): the singleton theme context for the stage + */ +StThemeContext * +st_theme_context_get_for_stage (ClutterStage *stage) +{ + StThemeContext *context; + + g_return_val_if_fail (CLUTTER_IS_STAGE (stage), NULL); + + context = g_object_get_data (G_OBJECT (stage), "st-theme-context"); + if (context) + return context; + + context = st_theme_context_new (); + g_object_set_data (G_OBJECT (stage), "st-theme-context", context); + g_signal_connect (stage, "destroy", + G_CALLBACK (on_stage_destroy), NULL); + + return context; +} + +/** + * st_theme_context_set_theme: + * @context: a #StThemeContext + * + * Sets the default set of theme stylesheets for the context. This theme will + * be used for the root node and for nodes descending from it, unless some other + * style is explicitely specified. + */ +void +st_theme_context_set_theme (StThemeContext *context, + StTheme *theme) +{ + g_return_if_fail (ST_IS_THEME_CONTEXT (context)); + g_return_if_fail (theme == NULL || ST_IS_THEME (theme)); + + if (context->theme != theme) + { + if (context->theme) + g_object_unref (context->theme); + + context->theme = theme; + + if (context->theme) + g_object_ref (context->theme); + + st_theme_context_changed (context); + } +} + +/** + * st_theme_context_get_theme: + * @context: a #StThemeContext + * + * Gets the default theme for the context. See st_theme_context_set_theme() + * + * Return value: (transfer none): the default theme for the context + */ +StTheme * +st_theme_context_get_theme (StThemeContext *context) +{ + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), NULL); + + return context->theme; +} + +/** + * st_theme_context_set_resolution: + * @context: a #StThemeContext + * @resolution: resolution of the context (number of pixels in an "inch") + * + * Sets the resolution of the theme context. This is the scale factor + * used to convert between points and the length units pt, in, and cm. + * This does not necessarily need to correspond to the actual number + * resolution of the device. A value of 72. means that points and + * pixels are identical. The default value is 96. + */ +void +st_theme_context_set_resolution (StThemeContext *context, + double resolution) +{ + g_return_if_fail (ST_IS_THEME_CONTEXT (context)); + + if (resolution == context->resolution) + return; + + context->resolution = resolution; + st_theme_context_changed (context); +} + +/** + * st_theme_context_set_resolution: + * @context: a #StThemeContext + * + * Gets the current resolution of the theme context. + * See st_theme_context_set_resolution(). + * + * Return value: the resolution (in dots-per-"inch") + */ +double +st_theme_context_get_resolution (StThemeContext *context) +{ + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), DEFAULT_RESOLUTION); + + return context->resolution; +} + +/** + * st_theme_context_set_font: + * @context: a #StThemeContext + * @font: the default font for theme context + * + * Sets the default font for the theme context. This is the font that + * is inherited by the root node of the tree of theme nodes. If the + * font is not overriden, then this font will be used. If the font is + * partially modified (for example, with 'font-size: 110%', then that + * modification is based on this font. + */ +void +st_theme_context_set_font (StThemeContext *context, + const PangoFontDescription *font) +{ + g_return_if_fail (ST_IS_THEME_CONTEXT (context)); + g_return_if_fail (font != NULL); + + if (context->font == font || + pango_font_description_equal (context->font, font)) + return; + + pango_font_description_free (context->font); + context->font = pango_font_description_copy (font); + st_theme_context_changed (context); +} + +/** + * st_theme_context_get_font: + * @context: a #StThemeContext + * + * Gets the default font for the theme context. See st_theme_context_set_font(). + * + * Return value: the default font for the theme context. + */ +const PangoFontDescription * +st_theme_context_get_font (StThemeContext *context) +{ + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), NULL); + + return context->font; +} + +/** + * st_theme_context_get_root_node: + * @context: a #StThemeContext + * + * Gets the root node of the tree of theme style nodes that associated with this + * context. For the node tree associated with a stage, this node represents + * styles applied to the stage itself. + * + * Return value: (transfer none): the root node of the context's style tree + */ +StThemeNode * +st_theme_context_get_root_node (StThemeContext *context) +{ + if (context->root_node == NULL) + context->root_node = st_theme_node_new (context, NULL, context->theme, + G_TYPE_NONE, NULL, NULL, NULL); + + return context->root_node; +} diff --git a/src/st/st-theme-context.h b/src/st/st-theme-context.h new file mode 100644 index 000000000..64d376451 --- /dev/null +++ b/src/st/st-theme-context.h @@ -0,0 +1,50 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __ST_THEME_CONTEXT_H__ +#define __ST_THEME_CONTEXT_H__ + +#include +#include +#include "st-theme-node.h" + +G_BEGIN_DECLS + +/** + * SECTION:StThemeContext + * @short_description: holds global information about a tree of styled objects + * + * #StThemeContext is responsible for managing information global to a tree of styled objects, + * such as the set of stylesheets or the default font. In normal usage, a #StThemeContext + * is bound to a #ClutterStage; a singleton #StThemeContext can be obtained for a #ClutterStage + * by using st_theme_context_get_for_stage(). + */ + +typedef struct _StThemeContextClass StThemeContextClass; + +#define ST_TYPE_THEME_CONTEXT (st_theme_context_get_type ()) +#define ST_THEME_CONTEXT(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), ST_TYPE_THEME_CONTEXT, StThemeContext)) +#define ST_THEME_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ST_TYPE_THEME_CONTEXT, StThemeContextClass)) +#define ST_IS_THEME_CONTEXT(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), ST_TYPE_THEME_CONTEXT)) +#define ST_IS_THEME_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ST_TYPE_THEME_CONTEXT)) +#define ST_THEME_CONTEXT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), ST_TYPE_THEME_CONTEXT, StThemeContextClass)) + +GType st_theme_context_get_type (void) G_GNUC_CONST; + +StThemeContext *st_theme_context_new (void); +StThemeContext *st_theme_context_get_for_stage (ClutterStage *stage); + +void st_theme_context_set_theme (StThemeContext *context, + StTheme *theme); +StTheme * st_theme_context_get_theme (StThemeContext *context); + +void st_theme_context_set_resolution (StThemeContext *context, + gdouble resolution); +double st_theme_context_get_resolution (StThemeContext *context); +void st_theme_context_set_font (StThemeContext *context, + const PangoFontDescription *font); +const PangoFontDescription *st_theme_context_get_font (StThemeContext *context); + +StThemeNode * st_theme_context_get_root_node (StThemeContext *context); + +G_END_DECLS + +#endif /* __ST_THEME_CONTEXT_H__ */ diff --git a/src/st/st-theme-image.c b/src/st/st-theme-image.c new file mode 100644 index 000000000..c70533f4c --- /dev/null +++ b/src/st/st-theme-image.c @@ -0,0 +1,92 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include + +#include "st-theme-image.h" + +struct _StThemeImage { + GObject parent; + + char *filename; + int border_top; + int border_right; + int border_bottom; + int border_left; +}; + +struct _StThemeImageClass { + GObjectClass parent_class; + +}; + +G_DEFINE_TYPE (StThemeImage, st_theme_image, G_TYPE_OBJECT) + +static void +st_theme_image_finalize (GObject *object) +{ + StThemeImage *image = ST_THEME_IMAGE (object); + + g_free (image->filename); + + G_OBJECT_CLASS (st_theme_image_parent_class)->finalize (object); +} + +static void +st_theme_image_class_init (StThemeImageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = st_theme_image_finalize; +} + +static void +st_theme_image_init (StThemeImage *image) +{ +} + +StThemeImage * +st_theme_image_new (const char *filename, + int border_top, + int border_right, + int border_bottom, + int border_left) +{ + StThemeImage *image; + + image = g_object_new (ST_TYPE_THEME_IMAGE, NULL); + + image->filename = g_strdup (filename); + image->border_top = border_top; + image->border_right = border_right; + image->border_bottom = border_bottom; + image->border_left = border_left; + + return image; +} + +const char * +st_theme_image_get_filename (StThemeImage *image) +{ + g_return_val_if_fail (ST_IS_THEME_IMAGE (image), NULL); + + return image->filename; +} + +void +st_theme_image_get_borders (StThemeImage *image, + int *border_top, + int *border_right, + int *border_bottom, + int *border_left) +{ + g_return_if_fail (ST_IS_THEME_IMAGE (image)); + + if (border_top) + *border_top = image->border_top; + if (border_right) + *border_right = image->border_right; + if (border_bottom) + *border_bottom = image->border_bottom; + if (border_left) + *border_left = image->border_left; +} diff --git a/src/st/st-theme-image.h b/src/st/st-theme-image.h new file mode 100644 index 000000000..a069f6551 --- /dev/null +++ b/src/st/st-theme-image.h @@ -0,0 +1,38 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __ST_THEME_IMAGE_H__ +#define __ST_THEME_IMAGE_H__ + +#include + +G_BEGIN_DECLS + +/* A StThemeImage encapsulates an image with specified unscaled borders on each edge. + */ +typedef struct _StThemeImage StThemeImage; +typedef struct _StThemeImageClass StThemeImageClass; + +#define ST_TYPE_THEME_IMAGE (st_theme_image_get_type ()) +#define ST_THEME_IMAGE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), ST_TYPE_THEME_IMAGE, StThemeImage)) +#define ST_THEME_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ST_TYPE_THEME_IMAGE, StThemeImageClass)) +#define ST_IS_THEME_IMAGE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), ST_TYPE_THEME_IMAGE)) +#define ST_IS_THEME_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ST_TYPE_THEME_IMAGE)) +#define ST_THEME_IMAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), ST_TYPE_THEME_IMAGE, StThemeImageClass)) + +GType st_theme_image_get_type (void) G_GNUC_CONST; + +StThemeImage *st_theme_image_new (const char *filename, + int border_top, + int border_right, + int border_bottom, + int border_left); + +const char *st_theme_image_get_filename (StThemeImage *image); +void st_theme_image_get_borders (StThemeImage *image, + int *border_top, + int *border_right, + int *border_bottom, + int *border_left); + +G_END_DECLS + +#endif /* __ST_THEME_IMAGE_H__ */ diff --git a/src/st/st-theme-node.c b/src/st/st-theme-node.c new file mode 100644 index 000000000..2b1b8f2d8 --- /dev/null +++ b/src/st/st-theme-node.c @@ -0,0 +1,1813 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include +#include + +#include "st-theme-private.h" +#include "st-theme-context.h" +#include "st-theme-node.h" + +static void st_theme_node_init (StThemeNode *node); +static void st_theme_node_class_init (StThemeNodeClass *klass); +static void st_theme_node_finalize (GObject *object); + +struct _StThemeNode { + GObject parent; + + StThemeContext *context; + StThemeNode *parent_node; + StTheme *theme; + + PangoFontDescription *font_desc; + + ClutterColor background_color; + ClutterColor foreground_color; + ClutterColor border_color[4]; + double border_width[4]; + guint padding[4]; + + char *background_image; + StThemeImage *background_theme_image; + + GType element_type; + char *element_id; + char *element_class; + char *pseudo_class; + + CRDeclaration **properties; + int n_properties; + + guint properties_computed : 1; + guint borders_computed : 1; + guint background_computed : 1; + guint foreground_computed : 1; + guint background_theme_image_computed : 1; + guint link_type : 2; +}; + +struct _StThemeNodeClass { + GObjectClass parent_class; + +}; + +static const ClutterColor BLACK_COLOR = { 0, 0, 0, 0xff }; +static const ClutterColor TRANSPARENT_COLOR = { 0, 0, 0, 0 }; + +G_DEFINE_TYPE (StThemeNode, st_theme_node, G_TYPE_OBJECT) + +static void +st_theme_node_init (StThemeNode *node) +{ +} + +static void +st_theme_node_class_init (StThemeNodeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = st_theme_node_finalize; +} + +static void +st_theme_node_finalize (GObject *object) +{ + StThemeNode *node = ST_THEME_NODE (object); + + g_free (node->element_id); + g_free (node->element_class); + g_free (node->pseudo_class); + + if (node->properties) + { + g_free (node->properties); + node->properties = NULL; + node->n_properties = 0; + } + + if (node->font_desc) + { + pango_font_description_free (node->font_desc); + node->font_desc = NULL; + } + + if (node->background_theme_image) + { + g_object_unref (node->background_theme_image); + node->background_theme_image = NULL; + } + + if (node->background_image) + g_free (node->background_image); + + G_OBJECT_CLASS (st_theme_node_parent_class)->finalize (object); +} + +/** + * st_theme_node_new: + * @context: the context representing global state for this themed tree + * @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 #StThemeNode. 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 + */ +StThemeNode * +st_theme_node_new (StThemeContext *context, + StThemeNode *parent_node, + StTheme *theme, + GType element_type, + const char *element_id, + const char *element_class, + const char *pseudo_class) +{ + StThemeNode *node; + + g_return_val_if_fail (ST_IS_THEME_CONTEXT (context), NULL); + g_return_val_if_fail (parent_node == NULL || ST_IS_THEME_NODE (parent_node), NULL); + + node = g_object_new (ST_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 = parent_node->theme; + + 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); + + return node; +} + +/** + * st_theme_node_get_parent: + * @node: a #StThemeNode + * + * Gets the parent themed element node. + * + * Return value: (transfer none): the parent #StThemeNode, or %NULL if this + * is the root node of the tree of theme elements. + */ +StThemeNode * +st_theme_node_get_parent (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->parent_node; +} + +/** + * st_theme_node_get_theme: + * @node: a #StThemeNode + * + * Gets the theme stylesheet set that styles this node + * + * Return value: (transfer none): the theme stylesheet set + */ +StTheme * +st_theme_node_get_theme (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->theme; +} + +GType +st_theme_node_get_element_type (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), G_TYPE_NONE); + + return node->element_type; +} + +const char * +st_theme_node_get_element_id (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->element_id; +} + +const char * +st_theme_node_get_element_class (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->element_class; +} + +const char * +st_theme_node_get_pseudo_class (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + return node->pseudo_class; +} + +static void +ensure_properties (StThemeNode *node) +{ + if (!node->properties_computed) + { + node->properties_computed = TRUE; + + if (node->theme) + _st_theme_get_matched_properties (node->theme, node, + &node->properties, &node->n_properties); + } +} + +typedef enum { + VALUE_FOUND, + VALUE_NOT_FOUND, + VALUE_INHERIT +} GetFromTermResult; + +static gboolean +term_is_inherit (CRTerm *term) +{ + return (term->type == TERM_IDENT && + strcmp (term->content.str->stryng->str, "inherit") == 0); +} + +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 int +color_component_from_double (double component) +{ + /* We want to spread the range 0-1 equally over 0..255, but + * 1.0 should map to 255 not 256, so we need to special-case it. + * See http://people.redhat.com/otaylor/pixel-converting.html + * for (very) detailed discussion of related issues. */ + if (component >= 1.0) + return 255; + else + return (int)(component * 256); +} + +static GetFromTermResult +get_color_from_rgba_term (CRTerm *term, + ClutterColor *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 = color_component_from_double (r); + color->green = color_component_from_double (g); + color->blue = color_component_from_double (b); + color->alpha = color_component_from_double (a); + + return VALUE_FOUND; +} + +static GetFromTermResult +get_color_from_term (StThemeNode *node, + CRTerm *term, + ClutterColor *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. + */ + else if (term->type == TERM_FUNCTION && + term->content.str && + term->content.str->stryng && + term->content.str->stryng->str && + strcmp (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; + color->green = rgb.green; + color->blue = rgb.blue; + color->alpha = 0xff; + + return VALUE_FOUND; +} + +/** + * st_theme_node_get_color: + * @node: a #StThemeNode + * @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: (out): 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 st_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 +st_theme_node_get_color (StThemeNode *node, + const char *property_name, + gboolean inherit, + ClutterColor *color) +{ + + int i; + + 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 st_theme_node_get_color (node->parent_node, property_name, inherit, color); + else + break; + } + } + } + + return FALSE; +} + +/** + * st_theme_node_get_double: + * @node: a #StThemeNode + * @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. + * + * 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 +st_theme_node_get_double (StThemeNode *node, + const char *property_name, + gboolean inherit, + double *value) +{ + gboolean result = FALSE; + int i; + + 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 = st_theme_node_get_double (node->parent_node, property_name, inherit, value); + + return result; +} + +static const PangoFontDescription * +get_parent_font (StThemeNode *node) +{ + if (node->parent_node) + return st_theme_node_get_font (node->parent_node); + else + return st_theme_context_get_font (node->context); +} + +static GetFromTermResult +get_length_from_term (StThemeNode *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 FALSE; + } + + 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: + g_warning ("length values must specify a unit"); + return VALUE_NOT_FOUND; + + 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 = st_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 = st_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 = st_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_internal (StThemeNode *node, + const char *property_name, + const char *suffixed, + gdouble *length) +{ + int i; + + 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; +} + +/** + * st_theme_node_get_length: + * @node: a #StThemeNode + * @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 st_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. + * + * 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 +st_theme_node_get_length (StThemeNode *node, + const char *property_name, + gboolean inherit, + gdouble *length) +{ + GetFromTermResult 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 && + st_theme_node_get_length (node->parent_node, property_name, inherit, length)) + return TRUE; + else + return FALSE; +} + +static void +do_border_property (StThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 6; /* Skip 'border' */ + StSide side = (StSide)-1; + ClutterColor color; + gboolean color_set = FALSE; + double width; + gboolean width_set = FALSE; + int j; + + if (g_str_has_prefix (property_name, "-left")) + { + side = ST_SIDE_LEFT; + property_name += 5; + } + else if (g_str_has_prefix (property_name, "-right")) + { + side = ST_SIDE_RIGHT; + property_name += 6; + } + else if (g_str_has_prefix (property_name, "-top")) + { + side = ST_SIDE_TOP; + property_name += 4; + } + else if (g_str_has_prefix (property_name, "-bottom")) + { + side = ST_SIDE_BOTTOM; + property_name += 7; + } + + if (strcmp (property_name, "") == 0) + { + /* Set value for width/color/node 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.; + 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, "solid") == 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 (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 (node, decl->value, FALSE, &width) == VALUE_FOUND) + /* Ignore inherit */ + width_set = TRUE; + } + + if (side == (StSide)-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; + } +} + +static void +do_padding_property_term (StThemeNode *node, + CRTerm *term, + gboolean left, + gboolean right, + gboolean top, + gboolean bottom) +{ + gdouble value; + + if (get_length_from_term (node, term, FALSE, &value) != VALUE_FOUND) + return; + + if (left) + node->padding[ST_SIDE_LEFT] = value; + if (right) + node->padding[ST_SIDE_RIGHT] = value; + if (top) + node->padding[ST_SIDE_TOP] = value; + if (bottom) + node->padding[ST_SIDE_BOTTOM] = value; +} + +static void +do_padding_property (StThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 7; /* Skip 'padding' */ + + 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_padding_property_term (node, decl->value, TRUE, TRUE, TRUE, TRUE); /* left/right/top/bottom */ + return; + } + else if (decl->value->next->next == NULL) + { /* 2 values */ + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, TRUE); /* top/bottom */ + do_padding_property_term (node, decl->value->next, TRUE, TRUE, FALSE, FALSE); /* left/right */ + } + else if (decl->value->next->next->next == NULL) + { /* 3 values */ + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); /* top */ + do_padding_property_term (node, decl->value->next, TRUE, TRUE, FALSE, FALSE); /* left/right */ + do_padding_property_term (node, decl->value->next->next, FALSE, FALSE, FALSE, TRUE); /* bottom */ + } + else if (decl->value->next->next->next == NULL) + { /* 4 values */ + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); + do_padding_property_term (node, decl->value->next, FALSE, TRUE, FALSE, FALSE); /* left */ + do_padding_property_term (node, decl->value->next->next, FALSE, FALSE, FALSE, TRUE); + do_padding_property_term (node, decl->value->next->next->next, TRUE, FALSE, FALSE, TRUE); /* left */ + } + else + { + g_warning ("Too many values for padding property"); + return; + } + } + else + { + if (decl->value == NULL || decl->value->next != NULL) + return; + + if (strcmp (property_name, "-left") == 0) + do_padding_property_term (node, decl->value, TRUE, FALSE, FALSE, FALSE); + else if (strcmp (property_name, "-right") == 0) + do_padding_property_term (node, decl->value, FALSE, TRUE, FALSE, FALSE); + else if (strcmp (property_name, "-top") == 0) + do_padding_property_term (node, decl->value, FALSE, FALSE, TRUE, FALSE); + else if (strcmp (property_name, "-bottom") == 0) + do_padding_property_term (node, decl->value, FALSE, FALSE, FALSE, TRUE); + } +} + +static void +ensure_borders (StThemeNode *node) +{ + int i, j; + + if (node->borders_computed) + return; + + node->borders_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); + else if (g_str_has_prefix (property_name, "padding")) + do_padding_property (node, decl); + } +} + +double +st_theme_node_get_border_width (StThemeNode *node, + StSide side) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (side >= ST_SIDE_LEFT && side <= ST_SIDE_BOTTOM, 0.); + + ensure_borders (node); + + return node->border_width[side]; +} + +static GetFromTermResult +get_background_color_from_term (StThemeNode *node, + CRTerm *term, + ClutterColor *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; +} + +static void +ensure_background (StThemeNode *node) +{ + int i; + + if (node->background_computed) + return; + + node->background_computed = TRUE; + node->background_color = TRANSPARENT_COLOR; + + 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; + g_free (node->background_image); + node->background_image = NULL; + + 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) + { + st_theme_node_get_background_color (node->parent_node, &node->background_color); + node->background_image = g_strdup (st_theme_node_get_background_image (node->parent_node)); + } + } + else if (term_is_none (term)) + { + /* leave node->background_color as transparent */ + } + else if (term->type == TERM_URI) + { + node->background_image = _st_theme_resolve_url (node->theme, + decl->parent_statement->parent_sheet, + term->content.str->stryng->str); + } + } + } + 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) + st_theme_node_get_background_color (node->parent_node, &node->background_color); + } + } + else if (strcmp (property_name, "-image") == 0) + { + if (decl->value == NULL || decl->value->next != NULL) + continue; + + if (decl->value->type == TERM_URI) + { + g_free (node->background_image); + node->background_image = _st_theme_resolve_url (node->theme, + decl->parent_statement->parent_sheet, + decl->value->content.str->stryng->str); + } + else if (term_is_inherit (decl->value)) + { + g_free (node->background_image); + node->background_image = g_strdup (st_theme_node_get_background_image (node->parent_node)); + } + else if (term_is_none (decl->value)) + { + g_free (node->background_image); + node->background_image = NULL; + } + } + } +} + +void +st_theme_node_get_background_color (StThemeNode *node, + ClutterColor *color) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + + ensure_background (node); + + *color = node->background_color; +} + +const char * +st_theme_node_get_background_image (StThemeNode *node) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), NULL); + + ensure_background (node); + + return node->background_image; +} + +void +st_theme_node_get_foreground_color (StThemeNode *node, + ClutterColor *color) +{ + g_return_if_fail (ST_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) + st_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 +st_theme_node_get_border_color (StThemeNode *node, + StSide side, + ClutterColor *color) +{ + g_return_if_fail (ST_IS_THEME_NODE (node)); + g_return_if_fail (side >= ST_SIDE_LEFT && side <= ST_SIDE_BOTTOM); + + ensure_borders (node); + + *color = node->border_color[side]; +} + +double +st_theme_node_get_padding (StThemeNode *node, + StSide side) +{ + g_return_val_if_fail (ST_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (side >= ST_SIDE_LEFT && side <= ST_SIDE_BOTTOM, 0.); + + ensure_borders (node); + + return node->padding[side]; +} + +StTextDecoration +st_theme_node_get_text_decoration (StThemeNode *node) +{ + int i; + + ensure_properties (node); + + for (i = node->n_properties - 1; i >= 0; i--) + { + CRDeclaration *decl = node->properties[i]; + + if (strcmp (decl->property->stryng->str, "text-decoration") == 0) + { + CRTerm *term = decl->value; + StTextDecoration decoration = 0; + + /* Specification is none | [ underline || overline || line-through || blink ] | inherit + * + * We're a bit more liberal, and for example treat 'underline none' as the same as + * none. + */ + for (; term; term = term->next) + { + if (term->type != TERM_IDENT) + goto next_decl; + + if (strcmp (term->content.str->stryng->str, "none") == 0) + { + return 0; + } + else if (strcmp (term->content.str->stryng->str, "inherit") == 0) + { + if (node->parent_node) + return st_theme_node_get_text_decoration (node->parent_node); + } + else if (strcmp (term->content.str->stryng->str, "underline") == 0) + { + decoration |= ST_TEXT_DECORATION_UNDERLINE; + } + else if (strcmp (term->content.str->stryng->str, "overline") == 0) + { + decoration |= ST_TEXT_DECORATION_OVERLINE; + } + else if (strcmp (term->content.str->stryng->str, "line-through") == 0) + { + decoration |= ST_TEXT_DECORATION_LINE_THROUGH; + } + else if (strcmp (term->content.str->stryng->str, "blink") == 0) + { + decoration |= ST_TEXT_DECORATION_BLINK; + } + else + { + goto next_decl; + } + } + + return decoration; + } + + next_decl: + ; + } + + return 0; +} + +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 (StThemeNode *node, + CRTerm *term, + double *size) +{ + if (term->type == TERM_IDENT) + { + double resolution = st_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)(term->content.num->val + 0.5); + + *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 * +st_theme_node_get_font (StThemeNode *node) +{ + PangoStyle font_style; + gboolean font_style_set = FALSE; + PangoVariant variant; + gboolean variant_set = FALSE; + PangoWeight weight; + gboolean weight_absolute; + gboolean weight_set = FALSE; + double parent_size; + double size = 0.; /* Suppress warning */ + gboolean size_set = FALSE; + char *family = NULL; + 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 = st_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); + + 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; +} + +/** + * st_theme_node_get_background_theme_image: + * @node: a #StThemeNode + * + * Gets the value for the -st-background-image style property + * + * Return value: (transfer none): the background image, or %NULL + * if there is no background theme image. + */ +StThemeImage * +st_theme_node_get_background_theme_image (StThemeNode *node) +{ + int i; + + if (node->background_theme_image_computed) + return node->background_theme_image; + + node->background_theme_image = NULL; + node->background_theme_image_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, "-st-background-image") == 0) + { + CRTerm *term = decl->value; + int lengths[4]; + int n_lengths = 0; + int i; + + const char *url; + int border_top; + int border_right; + int border_bottom; + int border_left; + + char *filename; + + /* First term must be the URL to the image */ + if (term->type != TERM_URI) + goto next_property; + + url = term->content.str->stryng->str; + + term = term->next; + + /* Followed by 0 to 4 lengths */ + for (i = 0; i < 4; i++) + { + double value; + + if (term == NULL) + break; + + if (get_length_from_term (node, term, FALSE, &value) != VALUE_FOUND) + goto next_property; + + lengths[n_lengths] = (int)(0.5 + value); + n_lengths++; + + term = term->next; + } + + switch (n_lengths) + { + case 0: + border_top = border_right = border_bottom = border_left = 0; + break; + case 1: + border_top = border_right = border_bottom = border_left = lengths[0]; + break; + case 2: + border_top = border_bottom = lengths[0]; + border_left = border_right = lengths[1]; + break; + case 3: + border_top = lengths[0]; + border_left = border_right = lengths[1]; + border_bottom = lengths[2]; + break; + case 4: + default: + border_top = lengths[0]; + border_right = lengths[1]; + border_bottom = lengths[2]; + border_left = lengths[3]; + break; + } + + filename = _st_theme_resolve_url (node->theme, decl->parent_statement->parent_sheet, url); + if (filename == NULL) + goto next_property; + + node->background_theme_image = st_theme_image_new (filename, + border_top, border_right, border_bottom, border_left); + + g_free (filename); + + return node->background_theme_image; + } + + next_property: + ; + } + + return NULL; +} diff --git a/src/st/st-theme-node.h b/src/st/st-theme-node.h new file mode 100644 index 000000000..623cea8ce --- /dev/null +++ b/src/st/st-theme-node.h @@ -0,0 +1,125 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __ST_THEME_NODE_H__ +#define __ST_THEME_NODE_H__ + +#include +#include "st-theme-image.h" + +G_BEGIN_DECLS + +/** + * SECTION:StThemeNode + * @short_description: style information for one node in a tree of themed objects + * + * A #StThemeNode represents the CSS style information (the set of CSS properties) for one + * node in a tree of themed objects. In typical usage, it represents the style information + * for a single #ClutterActor. A #StThemeNode is immutable: attributes such as the + * CSS classes for the node are passed in at construction. If the attributes of the node + * or any parent node change, the node should be discarded and a new node created. + * #StThemeNode has generic accessors to look up properties by name and specific + * accessors for standard CSS properties that add caching and handling of various + * details of the CSS specification. #StThemeNode also has convenience functions to help + * in implementing a #ClutterActor with borders and padding. + */ + +typedef struct _StTheme StTheme; +typedef struct _StThemeContext StThemeContext; + +typedef struct _StThemeNode StThemeNode; +typedef struct _StThemeNodeClass StThemeNodeClass; + +#define ST_TYPE_THEME_NODE (st_theme_node_get_type ()) +#define ST_THEME_NODE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), ST_TYPE_THEME_NODE, StThemeNode)) +#define ST_THEME_NODE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ST_TYPE_THEME_NODE, StThemeNodeClass)) +#define ST_IS_THEME_NODE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), ST_TYPE_THEME_NODE)) +#define ST_IS_THEME_NODE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ST_TYPE_THEME_NODE)) +#define ST_THEME_NODE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), ST_TYPE_THEME_NODE, StThemeNodeClass)) + +typedef enum { + ST_SIDE_LEFT, + ST_SIDE_RIGHT, + ST_SIDE_TOP, + ST_SIDE_BOTTOM +} StSide; + +/* These are the CSS values; that doesn't mean we have to implement blink... */ +typedef enum { + ST_TEXT_DECORATION_UNDERLINE = 1 << 0, + ST_TEXT_DECORATION_OVERLINE = 1 << 1, + ST_TEXT_DECORATION_LINE_THROUGH = 1 << 2, + ST_TEXT_DECORATION_BLINK = 1 << 3 +} StTextDecoration; + +GType st_theme_node_get_type (void) G_GNUC_CONST; + +StThemeNode *st_theme_node_new (StThemeContext *context, + StThemeNode *parent_node, /* can be null */ + StTheme *theme, /* can be null */ + GType element_type, + const char *element_id, + const char *element_class, + const char *pseudo_class); + +StThemeNode *st_theme_node_get_parent (StThemeNode *node); + +StTheme *st_theme_node_get_theme (StThemeNode *node); + +GType st_theme_node_get_element_type (StThemeNode *node); +const char *st_theme_node_get_element_id (StThemeNode *node); +const char *st_theme_node_get_element_class (StThemeNode *node); +const char *st_theme_node_get_pseudo_class (StThemeNode *node); + +/* Generic getters ... these are not cached so are less efficient. The other + * reason for adding the more specific version is that we can handle the + * details of the actual CSS rules, which can be complicated, especially + * for fonts + */ +gboolean st_theme_node_get_color (StThemeNode *node, + const char *property_name, + gboolean inherit, + ClutterColor *color); + +gboolean st_theme_node_get_double (StThemeNode *node, + const char *property_name, + gboolean inherit, + double *value); + +gboolean st_theme_node_get_length (StThemeNode *node, + const char *property_name, + gboolean inherit, + gdouble *length); + +/* Specific getters for particular properties: cached + */ +void st_theme_node_get_background_color (StThemeNode *node, + ClutterColor *color); +void st_theme_node_get_foreground_color (StThemeNode *node, + ClutterColor *color); + +const char *st_theme_node_get_background_image (StThemeNode *node); + +double st_theme_node_get_border_width (StThemeNode *node, + StSide side); +void st_theme_node_get_border_color (StThemeNode *node, + StSide side, + ClutterColor *color); +double st_theme_node_get_padding (StThemeNode *node, + StSide side); + +StTextDecoration st_theme_node_get_text_decoration (StThemeNode *node); + +/* Font rule processing is pretty complicated, so we just hardcode it + * under the standard font/font-family/font-size/etc names. This means + * you can't have multiple separate styled fonts for a single item, + * but that should be OK. + */ +const PangoFontDescription *st_theme_node_get_font (StThemeNode *node); + +/* This is the getter for -st-background-image, which is different from + * background-image in having provisions for unscaled borders. + */ +StThemeImage *st_theme_node_get_background_theme_image (StThemeNode *node); + +G_END_DECLS + +#endif /* __ST_THEME_NODE_H__ */ diff --git a/src/st/st-theme-private.h b/src/st/st-theme-private.h new file mode 100644 index 000000000..c2478e43e --- /dev/null +++ b/src/st/st-theme-private.h @@ -0,0 +1,22 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __ST_THEME_PRIVATE_H__ +#define __ST_THEME_PRIVATE_H__ + +#include +#include "st-theme.h" + +G_BEGIN_DECLS + +void _st_theme_get_matched_properties (StTheme *theme, + StThemeNode *node, + CRDeclaration ***properties, + int *n_properties); + +/* Resolve an URL from the stylesheet to a filename */ +char *_st_theme_resolve_url (StTheme *theme, + CRStyleSheet *base_stylesheet, + const char *url); + +G_END_DECLS + +#endif /* __ST_THEME_PRIVATE_H__ */ diff --git a/src/st/st-theme.c b/src/st/st-theme.c new file mode 100644 index 000000000..013b65db1 --- /dev/null +++ b/src/st/st-theme.c @@ -0,0 +1,971 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* This file started as a cut-and-paste of cr-sel-eng.c from libcroco. + * + * In moving it to hippo-canvas: + * - Reformatted and otherwise edited to match our coding style + * - Switched from handling xmlNode to handling HippoStyle + * - Simplified by removing things that we don't need or that don't + * make sense in our context. + * - The code to get a list of matching properties works quite differently; + * we order things in priority order, but we don't actually try to + * coalesce properties with the same name. + * + * In moving it to GNOME Shell: + * - Renamed again to StTheme + * - Reformatted to match the gnome-shell coding style + * - Removed notion of "theme engine" from hippo-canvas + * - pseudo-class matching changed from link enum to strings + * - Some code simplification + */ + +/* + * This file is part of The Croco Library + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of version 2.1 of the GNU Lesser General Public + * License as published by the Free Software Foundation. + * + * 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 Lesser + * General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 + * USA + * + * Copyright (C) 2003-2004 Dodji Seketeli. All Rights Reserved. + */ + + +#include +#include + +#include "st-theme-node.h" +#include "st-theme-private.h" + +static GObject *st_theme_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties); + +static void st_theme_finalize (GObject *object); +static void st_theme_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void st_theme_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); + +struct _StTheme +{ + GObject parent; + + char *application_stylesheet; + char *default_stylesheet; + char *theme_stylesheet; + + GHashTable *stylesheets_by_filename; + GHashTable *filenames_by_stylesheet; + + CRCascade *cascade; +}; + +struct _StThemeClass +{ + GObjectClass parent_class; +}; + +enum +{ + PROP_0, + PROP_APPLICATION_STYLESHEET, + PROP_THEME_STYLESHEET, + PROP_DEFAULT_STYLESHEET +}; + +G_DEFINE_TYPE (StTheme, st_theme, G_TYPE_OBJECT) + +/* Quick strcmp. Test only for == 0 or != 0, not < 0 or > 0. */ +#define strqcmp(str,lit,lit_len) \ + (strlen (str) != (lit_len) || memcmp (str, lit, lit_len)) + +static void +st_theme_init (StTheme *theme) +{ + theme->stylesheets_by_filename = g_hash_table_new_full (g_str_hash, g_str_equal, + (GDestroyNotify)g_free, (GDestroyNotify)cr_stylesheet_unref); + theme->filenames_by_stylesheet = g_hash_table_new (g_direct_hash, g_direct_equal); +} + +static void +st_theme_class_init (StThemeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = st_theme_constructor; + object_class->finalize = st_theme_finalize; + object_class->set_property = st_theme_set_property; + object_class->get_property = st_theme_get_property; + + /** + * StTheme:application-stylesheet: + * + * The highest priority stylesheet, representing application-specific + * styling; this is associated with the CSS "author" stylesheet. + */ + g_object_class_install_property (object_class, + PROP_APPLICATION_STYLESHEET, + g_param_spec_string ("application-stylesheet", + "Application Stylesheet", + "Stylesheet with application-specific styling", + NULL, + G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + + /** + * StTheme:theme-stylesheet: + * + * The second priority stylesheet, representing theme-specific styling; + * this is associated with the CSS "user" stylesheet. + */ + g_object_class_install_property (object_class, + PROP_THEME_STYLESHEET, + g_param_spec_string ("theme-stylesheet", + "Theme Stylesheet", + "Stylesheet with theme-specific styling", + NULL, + G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + + /** + * StTheme:default-stylesheet: + * + * The lowest priority stylesheet, representing global default + * styling; this is associated with the CSS "user agent" stylesheet. + */ + g_object_class_install_property (object_class, + PROP_DEFAULT_STYLESHEET, + g_param_spec_string ("default-stylesheet", + "Default Stylesheet", + "Stylesheet with global default styling", + NULL, + G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY)); + +} + +static CRStyleSheet * +parse_stylesheet (const char *filename) +{ + enum CRStatus status; + CRStyleSheet *stylesheet; + + if (filename == NULL) + return NULL; + + status = cr_om_parser_simply_parse_file ((const guchar *) filename, + CR_UTF_8, + &stylesheet); + + if (status != CR_OK) + { + g_warning ("Error parsing stylesheet '%s'", filename); + return NULL; + } + + return stylesheet; +} + +static void +insert_stylesheet (StTheme *theme, + const char *filename, + CRStyleSheet *stylesheet) +{ + char *filename_copy; + + if (stylesheet == NULL) + return; + + filename_copy = g_strdup(filename); + cr_stylesheet_ref (stylesheet); + + g_hash_table_insert (theme->stylesheets_by_filename, filename_copy, stylesheet); + g_hash_table_insert (theme->filenames_by_stylesheet, stylesheet, filename_copy); +} + +static GObject * +st_theme_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties) +{ + GObject *object; + StTheme *theme; + CRStyleSheet *application_stylesheet; + CRStyleSheet *theme_stylesheet; + CRStyleSheet *default_stylesheet; + + object = (*G_OBJECT_CLASS (st_theme_parent_class)->constructor) (type, + n_construct_properties, + construct_properties); + theme = ST_THEME (object); + + application_stylesheet = parse_stylesheet (theme->application_stylesheet); + theme_stylesheet = parse_stylesheet (theme->theme_stylesheet); + default_stylesheet = parse_stylesheet (theme->default_stylesheet); + + theme->cascade = cr_cascade_new (application_stylesheet, + theme_stylesheet, + default_stylesheet); + + if (theme->cascade == NULL) + g_error ("Out of memory when creating cascade object"); + + insert_stylesheet (theme, theme->application_stylesheet, application_stylesheet); + insert_stylesheet (theme, theme->theme_stylesheet, theme_stylesheet); + insert_stylesheet (theme, theme->default_stylesheet, default_stylesheet); + + return object; +} + +static void +st_theme_finalize (GObject * object) +{ + StTheme *theme = ST_THEME (object); + + g_hash_table_destroy (theme->stylesheets_by_filename); + g_hash_table_destroy (theme->filenames_by_stylesheet); + + g_free (theme->application_stylesheet); + g_free (theme->theme_stylesheet); + g_free (theme->default_stylesheet); + + if (theme->cascade) + { + cr_cascade_unref (theme->cascade); + theme->cascade = NULL; + } + + G_OBJECT_CLASS (st_theme_parent_class)->finalize (object); +} + +static void +st_theme_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + StTheme *theme = ST_THEME (object); + + switch (prop_id) + { + case PROP_APPLICATION_STYLESHEET: + { + const char *path = g_value_get_string (value); + + if (path != theme->application_stylesheet) + { + g_free (theme->application_stylesheet); + theme->application_stylesheet = g_strdup (path); + } + + break; + } + case PROP_THEME_STYLESHEET: + { + const char *path = g_value_get_string (value); + + if (path != theme->theme_stylesheet) + { + g_free (theme->theme_stylesheet); + theme->theme_stylesheet = g_strdup (path); + } + + break; + } + case PROP_DEFAULT_STYLESHEET: + { + const char *path = g_value_get_string (value); + + if (path != theme->default_stylesheet) + { + g_free (theme->default_stylesheet); + theme->default_stylesheet = g_strdup (path); + } + + break; + } + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +st_theme_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + StTheme *theme = ST_THEME (object); + + switch (prop_id) + { + case PROP_APPLICATION_STYLESHEET: + g_value_set_string (value, theme->application_stylesheet); + break; + case PROP_THEME_STYLESHEET: + g_value_set_string (value, theme->theme_stylesheet); + break; + case PROP_DEFAULT_STYLESHEET: + g_value_set_string (value, theme->default_stylesheet); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +/** + * st_theme_new: + * @application_stylesheet: The highest priority stylesheet, representing application-specific + * styling; this is associated with the CSS "author" stylesheet, may be %NULL + * @theme_stylesheet: The second priority stylesheet, representing theme-specific styling ; + * this is associated with the CSS "user" stylesheet, may be %NULL + * @default_stylesheet: The lowest priority stylesheet, representing global default styling; + * this is associated with the CSS "user agent" stylesheet, may be %NULL + * + * Return value: the newly created theme object + **/ +StTheme * +st_theme_new (const char *application_stylesheet, + const char *theme_stylesheet, + const char *default_stylesheet) +{ + StTheme *theme = g_object_new (ST_TYPE_THEME, + "application-stylesheet", application_stylesheet, + "theme-stylesheet", theme_stylesheet, + "default-stylesheet", default_stylesheet, + NULL); + + return theme; +} + +static gboolean +string_in_list (GString *stryng, + const char *list) +{ + const char *cur; + + for (cur = list; *cur;) + { + while (*cur && cr_utils_is_white_space (*cur)) + cur++; + + if (strncmp (cur, stryng->str, stryng->len) == 0) + { + cur += stryng->len; + if ((!*cur) || cr_utils_is_white_space (*cur)) + return TRUE; + } + + /* skip to next whitespace character */ + while (*cur && !cr_utils_is_white_space (*cur)) + cur++; + } + + return FALSE; +} + +static gboolean +pseudo_class_add_sel_matches_style (StTheme *a_this, + CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + const char *node_pseudo_class; + + g_return_val_if_fail (a_this + && a_add_sel + && a_add_sel->content.pseudo + && a_add_sel->content.pseudo->name + && a_add_sel->content.pseudo->name->stryng + && a_add_sel->content.pseudo->name->stryng->str + && a_node, FALSE); + + node_pseudo_class = st_theme_node_get_pseudo_class (a_node); + + if (node_pseudo_class == NULL) + return FALSE; + + return string_in_list (a_add_sel->content.pseudo->name->stryng, node_pseudo_class); +} + +/** + *@param a_add_sel the class additional selector to consider. + *@param a_node the style node to consider. + *@return TRUE if the class additional selector matches + *the style node given in argument, FALSE otherwise. + */ +static gboolean +class_add_sel_matches_style (CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + const char *element_class; + + g_return_val_if_fail (a_add_sel + && a_add_sel->type == CLASS_ADD_SELECTOR + && a_add_sel->content.class_name + && a_add_sel->content.class_name->stryng + && a_add_sel->content.class_name->stryng->str + && a_node, FALSE); + + element_class = st_theme_node_get_element_class (a_node); + if (element_class == NULL) + return FALSE; + + return string_in_list (a_add_sel->content.class_name->stryng, element_class); +} + +/** + *@return TRUE if the additional attribute selector matches + *the current style node given in argument, FALSE otherwise. + *@param a_add_sel the additional attribute selector to consider. + *@param a_node the style node to consider. + */ +static gboolean +id_add_sel_matches_style (CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + gboolean result = FALSE; + const char *id; + + g_return_val_if_fail (a_add_sel + && a_add_sel->type == ID_ADD_SELECTOR + && a_add_sel->content.id_name + && a_add_sel->content.id_name->stryng + && a_add_sel->content.id_name->stryng->str + && a_node, FALSE); + g_return_val_if_fail (a_add_sel + && a_add_sel->type == ID_ADD_SELECTOR + && a_node, FALSE); + + id = st_theme_node_get_element_id (a_node); + + if (id != NULL) + { + if (!strqcmp (id, a_add_sel->content.id_name->stryng->str, + a_add_sel->content.id_name->stryng->len)) + { + result = TRUE; + } + } + + return result; +} + +/** + *Evaluates if a given additional selector matches an style node. + *@param a_add_sel the additional selector to consider. + *@param a_node the style node to consider. + *@return TRUE is a_add_sel matches a_node, FALSE otherwise. + */ +static gboolean +additional_selector_matches_style (StTheme *a_this, + CRAdditionalSel *a_add_sel, + StThemeNode *a_node) +{ + CRAdditionalSel *cur_add_sel = NULL; + + g_return_val_if_fail (a_add_sel, FALSE); + + for (cur_add_sel = a_add_sel; cur_add_sel; cur_add_sel = cur_add_sel->next) + { + switch (cur_add_sel->type) + { + case NO_ADD_SELECTOR: + return FALSE; + case CLASS_ADD_SELECTOR: + if (!class_add_sel_matches_style (cur_add_sel, a_node)) + return FALSE; + break; + case ID_ADD_SELECTOR: + if (!id_add_sel_matches_style (cur_add_sel, a_node)) + return FALSE; + break; + case ATTRIBUTE_ADD_SELECTOR: + g_warning ("Attribute selectors not supported"); + return FALSE; + case PSEUDO_CLASS_ADD_SELECTOR: + if (!pseudo_class_add_sel_matches_style (a_this, cur_add_sel, a_node)) + return FALSE; + break; + } + } + + return TRUE; +} + +static gboolean +element_name_matches_type (const char *element_name, + GType element_type) +{ + if (element_type == G_TYPE_NONE) + { + return strcmp (element_name, "stage") == 0; + } + else + { + GType match_type = g_type_from_name (element_name); + if (match_type == G_TYPE_INVALID) + return FALSE; + + return g_type_is_a (element_type, match_type); + } +} + +/** + *Evaluate a selector (a simple selectors list) and says + *if it matches the style node given in parameter. + *The algorithm used here is the following: + *Walk the combinator separated list of simple selectors backward, starting + *from the end of the list. For each simple selector, looks if + *if matches the current style. + * + *@param a_this the selection engine. + *@param a_sel the simple selection list. + *@param a_node the style node. + *@param a_result out parameter. Set to true if the + *selector matches the style node, FALSE otherwise. + *@param a_recurse if set to TRUE, the function will walk to + *the next simple selector (after the evaluation of the current one) + *and recursively evaluate it. Must be usually set to TRUE unless you + *know what you are doing. + */ +static enum CRStatus +sel_matches_style_real (StTheme *a_this, + CRSimpleSel *a_sel, + StThemeNode *a_node, + gboolean *a_result, + gboolean a_eval_sel_list_from_end, + gboolean a_recurse) +{ + CRSimpleSel *cur_sel = NULL; + StThemeNode *cur_node = NULL; + GType cur_type; + + *a_result = FALSE; + + if (a_eval_sel_list_from_end) + { + /*go and get the last simple selector of the list */ + for (cur_sel = a_sel; cur_sel && cur_sel->next; cur_sel = cur_sel->next) + ; + } + else + { + cur_sel = a_sel; + } + + cur_node = a_node; + cur_type = st_theme_node_get_element_type (cur_node); + + while (cur_sel) + { + if (((cur_sel->type_mask & TYPE_SELECTOR) + && (cur_sel->name + && cur_sel->name->stryng + && cur_sel->name->stryng->str) + && + (element_name_matches_type (cur_sel->name->stryng->str, cur_type))) + || (cur_sel->type_mask & UNIVERSAL_SELECTOR)) + { + /* + *this simple selector + *matches the current style node + *Let's see if the preceding + *simple selectors also match + *their style node counterpart. + */ + if (cur_sel->add_sel) + { + if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node)) + goto walk_a_step_in_expr; + else + goto done; + } + else + goto walk_a_step_in_expr; + } + if (!(cur_sel->type_mask & TYPE_SELECTOR) + && !(cur_sel->type_mask & UNIVERSAL_SELECTOR)) + { + if (!cur_sel->add_sel) + goto done; + if (additional_selector_matches_style (a_this, cur_sel->add_sel, cur_node)) + goto walk_a_step_in_expr; + else + goto done; + } + else + { + goto done; + } + + walk_a_step_in_expr: + if (a_recurse == FALSE) + { + *a_result = TRUE; + goto done; + } + + /* + *here, depending on the combinator of cur_sel + *choose the axis of the element tree traversal + *and walk one step in the element tree. + */ + if (!cur_sel->prev) + break; + + switch (cur_sel->combinator) + { + case NO_COMBINATOR: + break; + + case COMB_WS: /*descendant selector */ + { + StThemeNode *n = NULL; + + /* + *walk the element tree upward looking for a parent + *style that matches the preceding selector. + */ + for (n = st_theme_node_get_parent (a_node); n; n = st_theme_node_get_parent (n)) + { + enum CRStatus status; + gboolean matches = FALSE; + + status = sel_matches_style_real (a_this, cur_sel->prev, n, &matches, FALSE, TRUE); + + if (status != CR_OK) + goto done; + + if (matches) + { + cur_node = n; + cur_type = st_theme_node_get_element_type (cur_node); + break; + } + } + + if (!n) + { + /* + *didn't find any ancestor that matches + *the previous simple selector. + */ + goto done; + } + /* + *in this case, the preceding simple sel + *will have been interpreted twice, which + *is a cpu and mem waste ... I need to find + *another way to do this. Anyway, this is + *my first attempt to write this function and + *I am a bit clueless. + */ + break; + } + + case COMB_PLUS: + g_warning ("+ combinators are not supported"); + goto done; + + case COMB_GT: + cur_node = st_theme_node_get_parent (cur_node); + if (!cur_node) + goto done; + cur_type = st_theme_node_get_element_type (cur_node); + break; + + default: + goto done; + } + + cur_sel = cur_sel->prev; + } + + /* + *if we reached this point, it means the selector matches + *the style node. + */ + *a_result = TRUE; + +done: + return CR_OK; +} + +static void +add_matched_properties (StTheme *a_this, + CRStyleSheet *a_nodesheet, + StThemeNode *a_node, + GPtrArray *props) +{ + CRStatement *cur_stmt = NULL; + CRSelector *sel_list = NULL; + CRSelector *cur_sel = NULL; + gboolean matches = FALSE; + enum CRStatus status = CR_OK; + + /* + *walk through the list of statements and, + *get the selectors list inside the statements that + *contain some, and try to match our style node in these + *selectors lists. + */ + for (cur_stmt = a_nodesheet->statements; cur_stmt; cur_stmt = cur_stmt->next) + { + /* + *initialyze the selector list in which we will + *really perform the search. + */ + sel_list = NULL; + + /* + *get the the damn selector list in + *which we have to look + */ + switch (cur_stmt->type) + { + case RULESET_STMT: + if (cur_stmt->kind.ruleset && cur_stmt->kind.ruleset->sel_list) + { + sel_list = cur_stmt->kind.ruleset->sel_list; + } + break; + + case AT_MEDIA_RULE_STMT: + if (cur_stmt->kind.media_rule + && cur_stmt->kind.media_rule->rulesets + && cur_stmt->kind.media_rule->rulesets->kind.ruleset + && cur_stmt->kind.media_rule->rulesets->kind.ruleset->sel_list) + { + sel_list = cur_stmt->kind.media_rule->rulesets->kind.ruleset->sel_list; + } + break; + + case AT_IMPORT_RULE_STMT: + { + CRAtImportRule *import_rule = cur_stmt->kind.import_rule; + + if (import_rule->sheet == NULL) + { + char *filename = NULL; + + if (import_rule->url->stryng && import_rule->url->stryng->str) + filename = _st_theme_resolve_url (a_this, + a_nodesheet, + import_rule->url->stryng->str); + + if (filename) + import_rule->sheet = parse_stylesheet (filename); + + if (import_rule->sheet) + { + insert_stylesheet (a_this, filename, import_rule->sheet); + /* refcount of stylesheets starts off at zero, so we don't need to unref! */ + } + else + { + /* Set a marker to avoid repeatedly trying to parse a non-existent or + * broken stylesheet + */ + import_rule->sheet = (CRStyleSheet *) - 1; + } + + if (filename) + g_free (filename); + } + + if (import_rule->sheet != (CRStyleSheet *) - 1) + { + add_matched_properties (a_this, import_rule->sheet, + a_node, props); + } + } + break; + default: + break; + } + + if (!sel_list) + continue; + + /* + *now, we have a comma separated selector list to look in. + *let's walk it and try to match the style node + *on each item of the list. + */ + for (cur_sel = sel_list; cur_sel; cur_sel = cur_sel->next) + { + if (!cur_sel->simple_sel) + continue; + + status = sel_matches_style_real (a_this, cur_sel->simple_sel, a_node, &matches, TRUE, TRUE); + + if (status == CR_OK && matches) + { + CRDeclaration *cur_decl = NULL; + + /* In order to sort the matching properties, we need to compute the + * specificity of the selector that actually matched this + * element. In a non-thread-safe fashion, we store it in the + * ruleset. (Fixing this would mean cut-and-pasting + * cr_simple_sel_compute_specificity(), and have no need for + * thread-safety anyways.) + * + * Once we've sorted the properties, the specificity no longer + * matters and it can be safely overriden. + */ + cr_simple_sel_compute_specificity (cur_sel->simple_sel); + + cur_stmt->specificity = cur_sel->simple_sel->specificity; + + for (cur_decl = cur_stmt->kind.ruleset->decl_list; cur_decl; cur_decl = cur_decl->next) + g_ptr_array_add (props, cur_decl); + } + } + } +} + +#define ORIGIN_AUTHOR_IMPORTANT (ORIGIN_AUTHOR + 1) +#define ORIGIN_USER_IMPORTANT (ORIGIN_AUTHOR + 2) + +static inline int +get_origin (const CRDeclaration * decl) +{ + enum CRStyleOrigin origin = decl->parent_statement->parent_sheet->origin; + + if (decl->important) + { + if (origin == ORIGIN_AUTHOR) + return ORIGIN_AUTHOR_IMPORTANT; + else if (origin == ORIGIN_USER) + return ORIGIN_USER_IMPORTANT; + } + + return origin; +} + +/* Order of comparison is so that higher priority statements compare after + * lower priority statements */ +static int +compare_declarations (gconstpointer a, + gconstpointer b) +{ + /* g_ptr_array_sort() is broooken */ + CRDeclaration *decl_a = *(CRDeclaration **) a; + CRDeclaration *decl_b = *(CRDeclaration **) b; + + int origin_a = get_origin (decl_a); + int origin_b = get_origin (decl_b); + + if (origin_a != origin_b) + return origin_a - origin_b; + + if (decl_a->parent_statement->specificity != decl_b->parent_statement->specificity) + return decl_a->parent_statement->specificity - decl_b->parent_statement->specificity; + + return 0; +} + +void +_st_theme_get_matched_properties (StTheme *theme, + StThemeNode *node, + CRDeclaration ***properties, + int *n_properties) +{ + enum CRStyleOrigin origin = 0; + CRStyleSheet *sheet = NULL; + GPtrArray *props = g_ptr_array_new (); + + g_return_if_fail (ST_IS_THEME (theme)); + g_return_if_fail (ST_IS_THEME_NODE (node)); + + for (origin = ORIGIN_UA; origin < NB_ORIGINS; origin++) + { + sheet = cr_cascade_get_sheet (theme->cascade, origin); + if (!sheet) + continue; + + add_matched_properties (theme, sheet, node, props); + } + + /* We count on a stable sort here so that later declarations come + * after earlier declarations */ + g_ptr_array_sort (props, compare_declarations); + + *n_properties = props->len; + *properties = (CRDeclaration **) g_ptr_array_free (props, FALSE); +} + +/* Resolve an url from an url() reference in a stylesheet into an absolute + * local filename, if possible. The resolution here is distinctly lame and + * will fail on many examples. + */ +char * +_st_theme_resolve_url (StTheme *theme, + CRStyleSheet *base_stylesheet, + const char *url) +{ + const char *base_filename = NULL; + char *dirname; + char *filename; + + /* Handle absolute file:/ URLs */ + if (g_str_has_prefix (url, "file:") || + g_str_has_prefix (url, "File:") || + g_str_has_prefix (url, "FILE:")) + { + GError *error = NULL; + char *filename; + + filename = g_filename_from_uri (url, NULL, &error); + if (filename == NULL) + { + g_warning ("%s", error->message); + g_error_free (error); + } + + return NULL; + } + + /* Guard against http:/ URLs */ + + if (g_str_has_prefix (url, "http:") || + g_str_has_prefix (url, "Http:") || + g_str_has_prefix (url, "HTTP:")) + { + g_warning ("Http URL '%s' in theme stylesheet is not supported", url); + return NULL; + } + + /* Assume anything else is a relative URL, and "resolve" it + */ + if (url[0] == '/') + return g_strdup (url); + + base_filename = g_hash_table_lookup (theme->filenames_by_stylesheet, base_stylesheet); + + if (base_filename == NULL) + { + g_warning ("Can't get base to resolve url '%s'", url); + return NULL; + } + + dirname = g_path_get_dirname (base_filename); + filename = g_build_filename (dirname, url, NULL); + g_free (dirname); + + return filename; +} diff --git a/src/st/st-theme.h b/src/st/st-theme.h new file mode 100644 index 000000000..dcf567a00 --- /dev/null +++ b/src/st/st-theme.h @@ -0,0 +1,38 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __ST_THEME_H__ +#define __ST_THEME_H__ + +#include + +#include "st-theme-node.h" + +G_BEGIN_DECLS + +/** + * SECTION:StTheme + * @short_description: a set of stylesheets + * + * #StTheme holds a set of stylesheets. (The "cascade" of the name + * Cascading Stylesheets.) A #StTheme can be set to apply to all the actors + * in a stage using st_theme_context_set_theme() or applied to a subtree + * of actors using st_widget_set_theme(). + */ + +typedef struct _StThemeClass StThemeClass; + +#define ST_TYPE_THEME (st_theme_get_type ()) +#define ST_THEME(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), ST_TYPE_THEME, StTheme)) +#define ST_THEME_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), ST_TYPE_THEME, StThemeClass)) +#define ST_IS_THEME(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), ST_TYPE_THEME)) +#define ST_IS_THEME_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), ST_TYPE_THEME)) +#define ST_THEME_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), ST_TYPE_THEME, StThemeClass)) + +GType st_theme_get_type (void) G_GNUC_CONST; + +StTheme *st_theme_new (const char *application_stylesheet, + const char *theme_stylesheet, + const char *default_stylesheet); + +G_END_DECLS + +#endif /* __ST_THEME_H__ */ diff --git a/src/st/test-theme.c b/src/st/test-theme.c new file mode 100644 index 000000000..555931dbd --- /dev/null +++ b/src/st/test-theme.c @@ -0,0 +1,292 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include +#include "st-theme.h" +#include "st-theme-context.h" +#include +#include + +static StThemeNode *root; +static StThemeNode *group1; +static StThemeNode *text1; +static StThemeNode *text2; +static StThemeNode *group2; +static StThemeNode *text3; +static StThemeNode *text4; +static StThemeNode *group3; +static StThemeNode *cairo_texture; +static gboolean fail; + +static const char *test; + +static void +assert_font (StThemeNode *node, + const char *node_description, + const char *expected) +{ + char *value = pango_font_description_to_string (st_theme_node_get_font (node)); + + if (strcmp (expected, value) != 0) + { + g_print ("%s: %s.font: expected: %s, got: %s\n", + test, node_description, expected, value); + fail = TRUE; + } + + g_free (value); +} + +static char * +text_decoration_to_string (StTextDecoration decoration) +{ + GString *result = g_string_new (NULL); + + if (decoration & ST_TEXT_DECORATION_UNDERLINE) + g_string_append(result, " underline"); + if (decoration & ST_TEXT_DECORATION_OVERLINE) + g_string_append(result, " overline"); + if (decoration & ST_TEXT_DECORATION_LINE_THROUGH) + g_string_append(result, " line_through"); + if (decoration & ST_TEXT_DECORATION_BLINK) + g_string_append(result, " blink"); + + if (result->len > 0) + g_string_erase (result, 0, 1); + else + g_string_append(result, "none"); + + return g_string_free (result, FALSE); +} + +static void +assert_text_decoration (StThemeNode *node, + const char *node_description, + StTextDecoration expected) +{ + StTextDecoration value = st_theme_node_get_text_decoration (node); + if (expected != value) + { + char *es = text_decoration_to_string (expected); + char *vs = text_decoration_to_string (value); + + g_print ("%s: %s.text-decoration: expected: %s, got: %s\n", + test, node_description, es, vs); + fail = TRUE; + + g_free (es); + g_free (vs); + } +} + +static void +assert_foreground_color (StThemeNode *node, + const char *node_description, + guint32 expected) +{ + ClutterColor color; + st_theme_node_get_foreground_color (node, &color); + guint32 value = clutter_color_to_pixel (&color); + + if (expected != value) + { + g_print ("%s: %s.color: expected: #%08x, got: #%08x\n", + test, node_description, expected, value); + fail = TRUE; + } +} + +static void +assert_background_color (StThemeNode *node, + const char *node_description, + guint32 expected) +{ + ClutterColor color; + st_theme_node_get_background_color (node, &color); + guint32 value = clutter_color_to_pixel (&color); + + if (expected != value) + { + g_print ("%s: %s.background-color: expected: #%08x, got: #%08x\n", + test, node_description, expected, value); + fail = TRUE; + } +} + +static void +assert_background_image (StThemeNode *node, + const char *node_description, + const char *expected) +{ + const char *value = st_theme_node_get_background_image (node); + if (expected == NULL) + expected = "(null)"; + if (value == NULL) + value = "(null)"; + + if (strcmp (expected, value) != 0) + { + g_print ("%s: %s.background-image: expected: %s, got: %s\n", + test, node_description, expected, value); + fail = TRUE; + } +} + +#define LENGTH_EPSILON 0.001 + +static void +assert_length (const char *node_description, + const char *property_description, + double expected, + double value) +{ + if (fabs (expected - value) > LENGTH_EPSILON) + { + g_print ("%s %s.%s: expected: %3f, got: %3f\n", + test, node_description, property_description, expected, value); + fail = TRUE; + } +} + +static void +test_defaults (void) +{ + test = "defaults"; + /* font comes from context */ + assert_font (root, "stage", "sans-serif 12"); + /* black is the default foreground color */ + assert_foreground_color (root, "stage", 0x00000ff); +} + +static void +test_lengths (void) +{ + test = "lengths"; + /* 12pt == 16px at 96dpi */ + assert_length ("group1", "padding-top", 16., + st_theme_node_get_padding (group1, ST_SIDE_TOP)); + /* 12px == 12px */ + assert_length ("group1", "padding-right", 12., + st_theme_node_get_padding (group1, ST_SIDE_RIGHT)); + /* 2em == 32px (with a 12pt font) */ + assert_length ("group1", "padding-bottom", 32., + st_theme_node_get_padding (group1, ST_SIDE_BOTTOM)); + /* 1in == 72pt == 96px, at 96dpi */ + assert_length ("group1", "padding-left", 96., + st_theme_node_get_padding (group1, ST_SIDE_LEFT)); +} + +static void +test_classes (void) +{ + test = "classes"; + /* .special-text class overrides size and style; + * the ClutterTexture.special-text selector doesn't match */ + assert_font (text1, "text1", "sans-serif Italic 32px"); +} + +static void +test_type_inheritance (void) +{ + test = "type_inheritance"; + /* From ClutterTexture element selector */ + assert_length ("cairoTexture", "padding-top", 10., + st_theme_node_get_padding (cairo_texture, ST_SIDE_TOP)); + /* From ClutterCairoTexture element selector */ + assert_length ("cairoTexture", "padding-right", 20., + st_theme_node_get_padding (cairo_texture, ST_SIDE_RIGHT)); +} + +static void +test_adjacent_selector (void) +{ + test = "adjacent_selector"; + /* #group1 > #text1 matches text1 */ + assert_foreground_color (text1, "text1", 0x00ff00ff); + /* stage > #text2 doesn't match text2 */ + assert_foreground_color (text2, "text2", 0x000000ff); +} + +static void +test_background (void) +{ + test = "background"; + /* group1 has a background: shortcut property setting color and image */ + assert_background_color (group1, "group1", 0xff0000ff); + assert_background_image (group1, "group1", "st/some-background.png"); + /* text1 inherits the background image but not the color */ + assert_background_color (text1, "text1", 0x00000000); + assert_background_image (text1, "text1", "st/some-background.png"); + /* text1 inherits inherits both, but then background: none overrides both */ + assert_background_color (text2, "text2", 0x00000000); + assert_background_image (text2, "text2", NULL); + /* background-image property */ + assert_background_image (group2, "group2", "st/other-background.png"); +} + +static void +test_font (void) +{ + test = "font"; + /* font specified with font: */ + assert_font (group2, "group2", "serif Italic 12px"); + /* text3 inherits and overrides individually properties */ + assert_font (text3, "text3", "serif Bold Oblique Small-Caps 24px"); +} + +static void +test_pseudo_class (void) +{ + test = "pseudo_class"; + /* text4 has :visited and :hover pseudo-classes, so should pick up both of these */ + assert_foreground_color (text4, "text4", 0x888888ff); + assert_text_decoration (text4, "text4", ST_TEXT_DECORATION_UNDERLINE); + /* :hover pseudo-class matches, but class doesn't match */ + assert_text_decoration (group3, "group3", 0); +} + +int +main (int argc, char **argv) +{ + StTheme *theme; + StThemeContext *context; + + clutter_init (&argc, &argv); + + theme = st_theme_new ("st/test-theme.css", + NULL, NULL); + + context = st_theme_context_new (); + st_theme_context_set_theme (context, theme); + st_theme_context_set_resolution (context, 96.); + st_theme_context_set_font (context, + pango_font_description_from_string ("sans-serif 12")); + + root = st_theme_context_get_root_node (context); + group1 = st_theme_node_new (context, root, NULL, + CLUTTER_TYPE_GROUP, "group1", NULL, NULL); + text1 = st_theme_node_new (context, group1, NULL, + CLUTTER_TYPE_TEXT, "text1", "special-text", NULL); + text2 = st_theme_node_new (context, group1, NULL, + CLUTTER_TYPE_TEXT, "text2", NULL, NULL); + group2 = st_theme_node_new (context, root, NULL, + CLUTTER_TYPE_GROUP, "group2", NULL, NULL); + text3 = st_theme_node_new (context, group2, NULL, + CLUTTER_TYPE_TEXT, "text3", NULL, NULL); + text4 = st_theme_node_new (context, group2, NULL, + CLUTTER_TYPE_TEXT, "text4", NULL, "visited hover"); + group3 = st_theme_node_new (context, group2, NULL, + CLUTTER_TYPE_GROUP, "group3", NULL, "hover"); + cairo_texture = st_theme_node_new (context, root, NULL, + CLUTTER_TYPE_CAIRO_TEXTURE, "cairoTexture", NULL, NULL); + + test_defaults (); + test_lengths (); + test_classes (); + test_type_inheritance (); + test_adjacent_selector (); + test_background (); + test_font (); + test_pseudo_class (); + + return fail ? 1 : 0; +} diff --git a/src/st/test-theme.css b/src/st/test-theme.css new file mode 100644 index 000000000..5776e0fe4 --- /dev/null +++ b/src/st/test-theme.css @@ -0,0 +1,68 @@ +stage { +} + +#group1 { + padding: 12pt; + padding-right: 12px; + padding-bottom: 2em; + padding-left: 1in; + + background: #ff0000 url('some-background.png'); +} + +#text1 { + background-image: inherit; +} + +.special-text { + font-size: 24pt; + font-style: italic; +} + +ClutterTexture.special-text { + font-weight: bold; +} + +#text2 { + background: inherit; + background: none; /* also overrides the color */ +} + +#group2 { + font: italic 12px serif; +} + +#text3 { + font-variant: small-caps; + font-weight: bold; + font-style: oblique; + font-size: 200%; +} + +ClutterTexture { + padding: 10px; +} + +ClutterCairoTexture { + padding-right: 20px; +} + +#group1 > #text1 { + color: #00ff00; +} + +stage > #text2 { + color: #ff0000; +} + +#group2 { + background-image: url('other-background.png'); +} + +ClutterText:hover { + text-decoration: underline; +} + +ClutterText:visited { + color: #888888; +}