From 98215f497d8aa22d600beaeedf6e3c8446209d76 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 => ShellTheme HippoCanvasThemeImage => ShellThemeImage HippoCanvasStyle => ShellThemeNode ShellThemeContext 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 --- .gitignore | 1 + configure.ac | 1 + src/Makefile-toolkit.am | 36 + src/Makefile.am | 9 +- src/toolkit/shell-theme-context.c | 218 ++++ src/toolkit/shell-theme-context.h | 40 + src/toolkit/shell-theme-image.c | 92 ++ src/toolkit/shell-theme-image.h | 38 + src/toolkit/shell-theme-node.c | 1777 +++++++++++++++++++++++++++++ src/toolkit/shell-theme-node.h | 115 ++ src/toolkit/shell-theme-private.h | 22 + src/toolkit/shell-theme.c | 1032 +++++++++++++++++ src/toolkit/shell-theme.h | 28 + src/toolkit/test-theme.c | 292 +++++ src/toolkit/test-theme.css | 68 ++ 15 files changed, 3766 insertions(+), 3 deletions(-) create mode 100644 src/Makefile-toolkit.am create mode 100644 src/toolkit/shell-theme-context.c create mode 100644 src/toolkit/shell-theme-context.h create mode 100644 src/toolkit/shell-theme-image.c create mode 100644 src/toolkit/shell-theme-image.h create mode 100644 src/toolkit/shell-theme-node.c create mode 100644 src/toolkit/shell-theme-node.h create mode 100644 src/toolkit/shell-theme-private.h create mode 100644 src/toolkit/shell-theme.c create mode 100644 src/toolkit/shell-theme.h create mode 100644 src/toolkit/test-theme.c create mode 100644 src/toolkit/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 70882e2b8..ad6b87a2a 100644 --- a/configure.ac +++ b/configure.ac @@ -60,6 +60,7 @@ PKG_CHECK_MODULES(TIDY, clutter-1.0) PKG_CHECK_MODULES(NBTK, 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-toolkit.am b/src/Makefile-toolkit.am new file mode 100644 index 000000000..67c485261 --- /dev/null +++ b/src/Makefile-toolkit.am @@ -0,0 +1,36 @@ +toolkit_cflags = \ + -I$(top_srcdir)/src \ + -DPREFIX=\""$(prefix)"\" \ + -DLIBDIR=\""$(libdir)"\" \ + -DG_DISABLE_DEPRECATED \ + -DG_LOG_DOMAIN=\"Shell\" \ + $(TOOLKIT_CFLAGS) + +# please, keep this sorted alphabetically +toolkit_sources = \ + toolkit/shell-theme.c \ + toolkit/shell-theme.h \ + toolkit/shell-theme-context.c \ + toolkit/shell-theme-context.h \ + toolkit/shell-theme-image.c \ + toolkit/shell-theme-image.h \ + toolkit/shell-theme-node.c \ + toolkit/shell-theme-node.h \ + toolkit/shell-theme-private.h + +non_gir_toolkit_sources = \ + toolkit/shell-theme-private.h + +noinst_LTLIBRARIES += libshell-toolkit.la + +libshell_toolkit_la_LIBADD = $(TOOLKIT_LIBS) +libshell_toolkit_la_SOURCES = $(toolkit_sources) $(toolkit_built_sources) +libshell_toolkit_la_CPPFLAGS = $(toolkit_cflags) +libshell_toolkit_la_LDFLAGS = $(LDADD) + +noinst_PROGRAMS += test-theme + +test_theme_CPPFLAGS = $(toolkit_cflags) +test_theme_LDADD = libshell-toolkit.la + +test_theme_SOURCES = toolkit/test-theme.c diff --git a/src/Makefile.am b/src/Makefile.am index 83837dfb6..dfa827c5d 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -4,6 +4,7 @@ CLEANFILES = EXTRA_DIST = libexec_PROGRAMS = noinst_LTLIBRARIES = +noinst_PROGRAMS = .AUTOPARALLEL: @@ -25,6 +26,7 @@ EXTRA_DIST += gnome-shell.in include Makefile-big.am include Makefile-gdmuser.am include Makefile-nbtk.am +include Makefile-toolkit.am include Makefile-tray.am gnome_shell_cflags = \ @@ -97,7 +99,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 = \ @@ -115,7 +117,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) @@ -126,7 +128,7 @@ test_recorder_SOURCES = \ endif BUILD_RECORDER libgnome_shell_la_gir_sources = \ - $(filter-out $(non_gir_sources), $(libgnome_shell_la_SOURCES)) + $(filter-out $(non_gir_sources) $(non_gir_toolkit_sources), $(libgnome_shell_la_SOURCES) $(toolkit_sources)) shell-marshal.h: stamp-shell-marshal.h @true @@ -155,6 +157,7 @@ libgnome_shell_la_LIBADD = \ libbig-1.0.la \ libnbtk-1.0.la \ libgdmuser-1.0.la \ + libshell-toolkit.la \ libtray.la libgnome_shell_la_CPPFLAGS = $(gnome_shell_cflags) diff --git a/src/toolkit/shell-theme-context.c b/src/toolkit/shell-theme-context.c new file mode 100644 index 000000000..62ad27691 --- /dev/null +++ b/src/toolkit/shell-theme-context.c @@ -0,0 +1,218 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include + +#include "shell-theme.h" +#include "shell-theme-context.h" + +struct _ShellThemeContext { + GObject parent; + + double resolution; + PangoFontDescription *font; + ShellThemeNode *root_node; + ShellTheme *theme; +}; + +struct _ShellThemeContextClass { + GObjectClass parent_class; +}; + +enum +{ + CHANGED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0, }; + +G_DEFINE_TYPE (ShellThemeContext, shell_theme_context, G_TYPE_OBJECT) + +static void +shell_theme_context_finalize (GObject *object) +{ + ShellThemeContext *context = SHELL_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 (shell_theme_context_parent_class)->finalize (object); +} + +static void +shell_theme_context_class_init (ShellThemeContextClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = shell_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 +shell_theme_context_init (ShellThemeContext *context) +{ + context->resolution = 96.; + context->font = pango_font_description_from_string ("sans-serif 10"); +} + +ShellThemeContext * +shell_theme_context_new (void) +{ + ShellThemeContext *context; + + context = g_object_new (SHELL_TYPE_THEME_CONTEXT, NULL); + + return context; +} + +static void +on_stage_destroy (ClutterStage *stage) +{ + ShellThemeContext *context = shell_theme_context_get_for_stage (stage); + + g_object_set_data (G_OBJECT (stage), "shell-theme-context", NULL); + g_object_unref (context); +} + +/** + * shell_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 + */ +ShellThemeContext * +shell_theme_context_get_for_stage (ClutterStage *stage) +{ + ShellThemeContext *context; + + g_return_val_if_fail (CLUTTER_IS_STAGE (stage), NULL); + + context = g_object_get_data (G_OBJECT (stage), "shell-theme-context"); + if (context) + return context; + + context = shell_theme_context_new (); + g_object_set_data (G_OBJECT (stage), "shell-theme-context", context); + g_signal_connect (stage, "destroy", + G_CALLBACK (on_stage_destroy), NULL); + + return context; +} + +/** + * shell_theme_context_set_theme: + * @context: a #ShellThemeContext + * + * 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 +shell_theme_context_set_theme (ShellThemeContext *context, + ShellTheme *theme) +{ + g_return_if_fail (SHELL_IS_THEME_CONTEXT (context)); + g_return_if_fail (theme == NULL || SHELL_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); + + g_signal_emit (context, signals[CHANGED], 0); + } +} + +/** + * shell_theme_context_get_theme: + * @context: a #ShellThemeContext + * + * Gets the default theme for the context. See shell_theme_context_set_theme() + * + * Return value: (transfer none): the default theme for the context + */ +ShellTheme * +shell_theme_context_get_theme (ShellThemeContext *context) +{ + g_return_val_if_fail (SHELL_IS_THEME_CONTEXT (context), NULL); + + return context->theme; +} + +void +shell_theme_context_set_resolution (ShellThemeContext *context, + double resolution) +{ + g_return_if_fail (SHELL_IS_THEME_CONTEXT (context)); + + context->resolution = resolution; +} + +double +shell_theme_context_get_resolution (ShellThemeContext *context) +{ + g_return_val_if_fail (SHELL_IS_THEME_CONTEXT (context), 96.); + + return context->resolution; +} + +void +shell_theme_context_set_font (ShellThemeContext *context, + const PangoFontDescription *font) +{ + g_return_if_fail (SHELL_IS_THEME_CONTEXT (context)); + + if (context->font == font) + return; + + pango_font_description_free (context->font); + context->font = pango_font_description_copy (font); +} + +const PangoFontDescription * +shell_theme_context_get_font (ShellThemeContext *context) +{ + g_return_val_if_fail (SHELL_IS_THEME_CONTEXT (context), NULL); + + return context->font; +} + +/** + * shell_theme_context_get_root_node: + * @context: a #ShellThemeContext + * + * 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 + */ +ShellThemeNode * +shell_theme_context_get_root_node (ShellThemeContext *context) +{ + if (context->root_node == NULL) + context->root_node = shell_theme_node_new (context, NULL, context->theme, + G_TYPE_NONE, NULL, NULL, NULL); + + return context->root_node; +} diff --git a/src/toolkit/shell-theme-context.h b/src/toolkit/shell-theme-context.h new file mode 100644 index 000000000..c74b1db62 --- /dev/null +++ b/src/toolkit/shell-theme-context.h @@ -0,0 +1,40 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_THEME_CONTEXT_H__ +#define __SHELL_THEME_CONTEXT_H__ + +#include +#include +#include "shell-theme-node.h" + +G_BEGIN_DECLS + +typedef struct _ShellThemeContextClass ShellThemeContextClass; + +#define SHELL_TYPE_THEME_CONTEXT (shell_theme_context_get_type ()) +#define SHELL_THEME_CONTEXT(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_THEME_CONTEXT, ShellThemeContext)) +#define SHELL_THEME_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_THEME_CONTEXT, ShellThemeContextClass)) +#define SHELL_IS_THEME_CONTEXT(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_THEME_CONTEXT)) +#define SHELL_IS_THEME_CONTEXT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_THEME_CONTEXT)) +#define SHELL_THEME_CONTEXT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_THEME_CONTEXT, ShellThemeContextClass)) + +GType shell_theme_context_get_type (void) G_GNUC_CONST; + +ShellThemeContext *shell_theme_context_new (void); +ShellThemeContext *shell_theme_context_get_for_stage (ClutterStage *stage); + +void shell_theme_context_set_theme (ShellThemeContext *context, + ShellTheme *theme); +ShellTheme * shell_theme_context_get_theme (ShellThemeContext *context); + +void shell_theme_context_set_resolution (ShellThemeContext *context, + gdouble resolution); +double shell_theme_context_get_resolution (ShellThemeContext *context); +void shell_theme_context_set_font (ShellThemeContext *context, + const PangoFontDescription *font); +const PangoFontDescription *shell_theme_context_get_font (ShellThemeContext *context); + +ShellThemeNode * shell_theme_context_get_root_node (ShellThemeContext *context); + +G_END_DECLS + +#endif /* __SHELL_THEME_CONTEXT_H__ */ diff --git a/src/toolkit/shell-theme-image.c b/src/toolkit/shell-theme-image.c new file mode 100644 index 000000000..e07306f90 --- /dev/null +++ b/src/toolkit/shell-theme-image.c @@ -0,0 +1,92 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include + +#include "shell-theme-image.h" + +struct _ShellThemeImage { + GObject parent; + + char *filename; + int border_top; + int border_right; + int border_bottom; + int border_left; +}; + +struct _ShellThemeImageClass { + GObjectClass parent_class; + +}; + +G_DEFINE_TYPE (ShellThemeImage, shell_theme_image, G_TYPE_OBJECT) + +static void +shell_theme_image_finalize (GObject *object) +{ + ShellThemeImage *image = SHELL_THEME_IMAGE (object); + + g_free (image->filename); + + G_OBJECT_CLASS (shell_theme_image_parent_class)->finalize (object); +} + +static void +shell_theme_image_class_init (ShellThemeImageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = shell_theme_image_finalize; +} + +static void +shell_theme_image_init (ShellThemeImage *image) +{ +} + +ShellThemeImage * +shell_theme_image_new (const char *filename, + int border_top, + int border_right, + int border_bottom, + int border_left) +{ + ShellThemeImage *image; + + image = g_object_new (SHELL_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 * +shell_theme_image_get_filename (ShellThemeImage *image) +{ + g_return_val_if_fail (SHELL_IS_THEME_IMAGE (image), NULL); + + return image->filename; +} + +void +shell_theme_image_get_borders (ShellThemeImage *image, + int *border_top, + int *border_right, + int *border_bottom, + int *border_left) +{ + g_return_if_fail (SHELL_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/toolkit/shell-theme-image.h b/src/toolkit/shell-theme-image.h new file mode 100644 index 000000000..e6b1a9517 --- /dev/null +++ b/src/toolkit/shell-theme-image.h @@ -0,0 +1,38 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_THEME_IMAGE_H__ +#define __SHELL_THEME_IMAGE_H__ + +#include + +G_BEGIN_DECLS + +/* A ShellThemeImage encapsulates an image with specified unscaled borders on each edge. + */ +typedef struct _ShellThemeImage ShellThemeImage; +typedef struct _ShellThemeImageClass ShellThemeImageClass; + +#define SHELL_TYPE_THEME_IMAGE (shell_theme_image_get_type ()) +#define SHELL_THEME_IMAGE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_THEME_IMAGE, ShellThemeImage)) +#define SHELL_THEME_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_THEME_IMAGE, ShellThemeImageClass)) +#define SHELL_IS_THEME_IMAGE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_THEME_IMAGE)) +#define SHELL_IS_THEME_IMAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_THEME_IMAGE)) +#define SHELL_THEME_IMAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_THEME_IMAGE, ShellThemeImageClass)) + +GType shell_theme_image_get_type (void) G_GNUC_CONST; + +ShellThemeImage *shell_theme_image_new (const char *filename, + int border_top, + int border_right, + int border_bottom, + int border_left); + +const char *shell_theme_image_get_filename (ShellThemeImage *image); +void shell_theme_image_get_borders (ShellThemeImage *image, + int *border_top, + int *border_right, + int *border_bottom, + int *border_left); + +G_END_DECLS + +#endif /* __SHELL_THEME_IMAGE_H__ */ diff --git a/src/toolkit/shell-theme-node.c b/src/toolkit/shell-theme-node.c new file mode 100644 index 000000000..474e3510d --- /dev/null +++ b/src/toolkit/shell-theme-node.c @@ -0,0 +1,1777 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include +#include + +#include "shell-theme-private.h" +#include "shell-theme-context.h" +#include "shell-theme-node.h" + +static void shell_theme_node_init (ShellThemeNode *node); +static void shell_theme_node_class_init (ShellThemeNodeClass *klass); +static void shell_theme_node_dispose (GObject *object); +static void shell_theme_node_finalize (GObject *object); + + +#if 0 +enum { + LAST_SIGNAL +}; + +static int signals[LAST_SIGNAL]; +#endif + +struct _ShellThemeNode { + GObject parent; + + ShellThemeContext *context; + ShellThemeNode *parent_node; + ShellTheme *theme; + + PangoFontDescription *font_desc; + + ClutterColor background_color; + ClutterColor foreground_color; + ClutterColor border_color[4]; + double border_width[4]; + guint padding[4]; + + char *background_image; + ShellThemeImage *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 _ShellThemeNodeClass { + GObjectClass parent_class; + +}; + +static const ClutterColor BLACK_COLOR = { 0, 0, 0, 0xff }; +static const ClutterColor TRANSPARENT_COLOR = { 0, 0, 0, 0 }; + +G_DEFINE_TYPE (ShellThemeNode, shell_theme_node, G_TYPE_OBJECT) + +static void +shell_theme_node_init (ShellThemeNode *node) +{ +} + +static void +shell_theme_node_class_init (ShellThemeNodeClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = shell_theme_node_dispose; + object_class->finalize = shell_theme_node_finalize; +} + +static void +shell_theme_node_dispose (GObject *object) +{ + /* ShellThemeNode *node = SHELL_THEME_NODE (object); */ + + G_OBJECT_CLASS (shell_theme_node_parent_class)->dispose (object); +} + +static void +shell_theme_node_finalize (GObject *object) +{ + ShellThemeNode *node = SHELL_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 (shell_theme_node_parent_class)->finalize (object); +} + +ShellThemeNode * +shell_theme_node_new (ShellThemeContext *context, + ShellThemeNode *parent_node, + ShellTheme *theme, + GType element_type, + const char *element_id, + const char *element_class, + const char *pseudo_class) +{ + ShellThemeNode *node; + + g_return_val_if_fail (SHELL_IS_THEME_CONTEXT (context), NULL); + g_return_val_if_fail (parent_node == NULL || SHELL_IS_THEME_NODE (parent_node), NULL); + + node = g_object_new (SHELL_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; +} + +/** + * shell_theme_node_get_parent: + * @node: a #ShellThemeNode + * + * Gets the parent themed element node. + * + * Return value: (transfer none): the parent #ShellThemeNode, or %NULL if this + * is the root node of the tree of theme elements. + */ +ShellThemeNode * +shell_theme_node_get_parent (ShellThemeNode *node) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), NULL); + + return node->parent_node; +} + +/** + * shell_theme_node_get_theme: + * @node: a #ShellThemeNode + * + * Gets the theme stylesheet set that styles this node + * + * Return value: (transfer none): the theme stylesheet set + */ +ShellTheme * +shell_theme_node_get_theme (ShellThemeNode *node) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), NULL); + + return node->theme; +} + +GType +shell_theme_node_get_element_type (ShellThemeNode *node) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), G_TYPE_NONE); + + return node->element_type; +} + +const char * +shell_theme_node_get_element_id (ShellThemeNode *node) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), NULL); + + return node->element_id; +} + +const char * +shell_theme_node_get_element_class (ShellThemeNode *node) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), NULL); + + return node->element_class; +} + +const char * +shell_theme_node_get_pseudo_class (ShellThemeNode *node) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), NULL); + + return node->pseudo_class; +} + +static void +ensure_properties (ShellThemeNode *node) +{ + if (!node->properties_computed) + { + node->properties_computed = TRUE; + + if (node->theme) + _shell_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) +{ + /* http://people.redhat.com/otaylor/pixel-converting.html */ + 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 (ShellThemeNode *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; +} + +gboolean +shell_theme_node_get_color (ShellThemeNode *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 shell_theme_node_get_color (node->parent_node, property_name, inherit, color); + else + break; + } + } + } + + return FALSE; +} + +gboolean +shell_theme_node_get_double (ShellThemeNode *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 = shell_theme_node_get_double (node->parent_node, property_name, inherit, value); + + return result; +} + +static const PangoFontDescription * +get_parent_font (ShellThemeNode *node) +{ + if (node->parent_node) + return shell_theme_node_get_font (node->parent_node); + else + return shell_theme_context_get_font (node->context); +} + +static GetFromTermResult +get_length_from_term (ShellThemeNode *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 = shell_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 = shell_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 = shell_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 (ShellThemeNode *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; +} + +gboolean +shell_theme_node_get_length (ShellThemeNode *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 && + shell_theme_node_get_length (node->parent_node, property_name, inherit, length)) + return TRUE; + else + return FALSE; +} + +static void +do_border_property (ShellThemeNode *node, + CRDeclaration *decl) +{ + const char *property_name = decl->property->stryng->str + 6; /* Skip 'border' */ + ShellSide side = (ShellSide)-1; + ClutterColor color; + gboolean color_set = FALSE; + double width; + gboolean width_set = FALSE; + int j; + + if (g_str_has_prefix (property_name, "-left")) + { + side = SHELL_SIDE_LEFT; + property_name += 5; + } + else if (g_str_has_prefix (property_name, "-right")) + { + side = SHELL_SIDE_RIGHT; + property_name += 6; + } + else if (g_str_has_prefix (property_name, "-top")) + { + side = SHELL_SIDE_TOP; + property_name += 4; + } + else if (g_str_has_prefix (property_name, "-bottom")) + { + side = SHELL_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 == (ShellSide)-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 (ShellThemeNode *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[SHELL_SIDE_LEFT] = value; + if (right) + node->padding[SHELL_SIDE_RIGHT] = value; + if (top) + node->padding[SHELL_SIDE_TOP] = value; + if (bottom) + node->padding[SHELL_SIDE_BOTTOM] = value; +} + +static void +do_padding_property (ShellThemeNode *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 (ShellThemeNode *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 +shell_theme_node_get_border_width (ShellThemeNode *node, + ShellSide side) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (side >= SHELL_SIDE_LEFT && side <= SHELL_SIDE_BOTTOM, 0.); + + ensure_borders (node); + + return node->border_width[side]; +} + +static GetFromTermResult +get_background_color_from_term (ShellThemeNode *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 (ShellThemeNode *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) + { + } + else if (result == VALUE_INHERIT) + { + if (node->parent_node) + { + shell_theme_node_get_background_color (node->parent_node, &node->background_color); + node->background_image = g_strdup (shell_theme_node_get_background_image (node->parent_node)); + } + } + else if (term_is_none (term)) + { + } + else if (term->type == TERM_URI) + { + node->background_image = _shell_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) + { + } + else if (result == VALUE_INHERIT) + { + if (node->parent_node) + shell_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 = _shell_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 (shell_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 +shell_theme_node_get_background_color (ShellThemeNode *node, + ClutterColor *color) +{ + g_return_if_fail (SHELL_IS_THEME_NODE (node)); + + ensure_background (node); + + *color = node->background_color; +} + +const char * +shell_theme_node_get_background_image (ShellThemeNode *node) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), NULL); + + ensure_background (node); + + return node->background_image; +} + +void +shell_theme_node_get_foreground_color (ShellThemeNode *node, + ClutterColor *color) +{ + g_return_if_fail (SHELL_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) + shell_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 +shell_theme_node_get_border_color (ShellThemeNode *node, + ShellSide side, + ClutterColor *color) +{ + g_return_if_fail (SHELL_IS_THEME_NODE (node)); + g_return_if_fail (side >= SHELL_SIDE_LEFT && side <= SHELL_SIDE_BOTTOM); + + ensure_borders (node); + + *color = node->border_color[side]; +} + +double +shell_theme_node_get_padding (ShellThemeNode *node, + ShellSide side) +{ + g_return_val_if_fail (SHELL_IS_THEME_NODE (node), 0.); + g_return_val_if_fail (side >= SHELL_SIDE_LEFT && side <= SHELL_SIDE_BOTTOM, 0.); + + ensure_borders (node); + + return node->padding[side]; +} + +ShellTextDecoration +shell_theme_node_get_text_decoration (ShellThemeNode *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; + ShellTextDecoration 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 shell_theme_node_get_text_decoration (node->parent_node); + } + else if (strcmp (term->content.str->stryng->str, "underline") == 0) + { + decoration |= SHELL_TEXT_DECORATION_UNDERLINE; + } + else if (strcmp (term->content.str->stryng->str, "overline") == 0) + { + decoration |= SHELL_TEXT_DECORATION_OVERLINE; + } + else if (strcmp (term->content.str->stryng->str, "line-through") == 0) + { + decoration |= SHELL_TEXT_DECORATION_LINE_THROUGH; + } + else if (strcmp (term->content.str->stryng->str, "blink") == 0) + { + decoration |= SHELL_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 concetenate to 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 (ShellThemeNode *node, + CRTerm *term, + double *size) +{ + if (term->type == TERM_IDENT) + { + double resolution = shell_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 * +shell_theme_node_get_font (ShellThemeNode *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 = shell_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)) + ; + else if (font_variant_from_term (term, &tmp_variant)) + ; + else if (font_weight_from_term (term, &tmp_weight, &tmp_weight_absolute)) + ; + else + 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; +} + +/** + * shell_theme_node_get_background_theme_image: + * @node: a #ShellThemeNode + * + * Gets the value for the -shell-background-image style property + * + * Return value: (transfer none): the background image, or %NULL + * if there is no background theme image. + */ +ShellThemeImage * +shell_theme_node_get_background_theme_image (ShellThemeNode *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, "-shell-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 = _shell_theme_resolve_url (node->theme, decl->parent_statement->parent_sheet, url); + if (filename == NULL) + goto next_property; + + node->background_theme_image = shell_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/toolkit/shell-theme-node.h b/src/toolkit/shell-theme-node.h new file mode 100644 index 000000000..b98b03754 --- /dev/null +++ b/src/toolkit/shell-theme-node.h @@ -0,0 +1,115 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_THEME_NODE_H__ +#define __SHELL_THEME_NODE_H__ + +#include +#include "shell-theme-image.h" + +G_BEGIN_DECLS + +typedef struct _ShellTheme ShellTheme; +typedef struct _ShellThemeContext ShellThemeContext; + +typedef struct _ShellThemeNode ShellThemeNode; +typedef struct _ShellThemeNodeClass ShellThemeNodeClass; + +#define SHELL_TYPE_THEME_NODE (shell_theme_node_get_type ()) +#define SHELL_THEME_NODE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_THEME_NODE, ShellThemeNode)) +#define SHELL_THEME_NODE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_THEME_NODE, ShellThemeNodeClass)) +#define SHELL_IS_THEME_NODE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_THEME_NODE)) +#define SHELL_IS_THEME_NODE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_THEME_NODE)) +#define SHELL_THEME_NODE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_THEME_NODE, ShellThemeNodeClass)) + +typedef enum { + SHELL_SIDE_LEFT, + SHELL_SIDE_RIGHT, + SHELL_SIDE_TOP, + SHELL_SIDE_BOTTOM +} ShellSide; + +/* These are the CSS values; that doesn't mean we have to implement blink... */ +typedef enum { + SHELL_TEXT_DECORATION_UNDERLINE = 1 << 0, + SHELL_TEXT_DECORATION_OVERLINE = 1 << 1, + SHELL_TEXT_DECORATION_LINE_THROUGH = 1 << 2, + SHELL_TEXT_DECORATION_BLINK = 1 << 3 +} ShellTextDecoration; + +GType shell_theme_node_get_type (void) G_GNUC_CONST; + +/* An element_type of G_TYPE_NONE means this style was created for the stage + * actor and matches a selector element name of 'stage' + */ +ShellThemeNode *shell_theme_node_new (ShellThemeContext *context, + ShellThemeNode *parent_node, /* can be null */ + ShellTheme *theme, /* can be null */ + GType element_type, + const char *element_id, + const char *element_class, + const char *pseudo_class); + +ShellThemeNode *shell_theme_node_get_parent (ShellThemeNode *node); + +ShellTheme *shell_theme_node_get_theme (ShellThemeNode *node); + +GType shell_theme_node_get_element_type (ShellThemeNode *node); +const char *shell_theme_node_get_element_id (ShellThemeNode *node); +const char *shell_theme_node_get_element_class (ShellThemeNode *node); +const char *shell_theme_node_get_pseudo_class (ShellThemeNode *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 shell_theme_node_get_color (ShellThemeNode *node, + const char *property_name, + gboolean inherit, + ClutterColor *color); + +gboolean shell_theme_node_get_double (ShellThemeNode *node, + const char *property_name, + gboolean inherit, + double *value); + +/* The length here is already resolved to pixels + */ +gboolean shell_theme_node_get_length (ShellThemeNode *node, + const char *property_name, + gboolean inherit, + gdouble *length); + +/* Specific getters for particular properties: cached + */ +void shell_theme_node_get_background_color (ShellThemeNode *node, + ClutterColor *color); +void shell_theme_node_get_foreground_color (ShellThemeNode *node, + ClutterColor *color); + +const char *shell_theme_node_get_background_image (ShellThemeNode *node); + +double shell_theme_node_get_border_width (ShellThemeNode *node, + ShellSide side); +void shell_theme_node_get_border_color (ShellThemeNode *node, + ShellSide side, + ClutterColor *color); +double shell_theme_node_get_padding (ShellThemeNode *node, + ShellSide side); + +ShellTextDecoration shell_theme_node_get_text_decoration (ShellThemeNode *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 *shell_theme_node_get_font (ShellThemeNode *node); + +/* This is the getter for -shell-background-image, which is different from + * background-image in having provisions for unscaled borders. + */ +ShellThemeImage *shell_theme_node_get_background_theme_image (ShellThemeNode *node); + +G_END_DECLS + +#endif /* __SHELL_THEME_NODE_H__ */ diff --git a/src/toolkit/shell-theme-private.h b/src/toolkit/shell-theme-private.h new file mode 100644 index 000000000..214e94dcc --- /dev/null +++ b/src/toolkit/shell-theme-private.h @@ -0,0 +1,22 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_THEME_PRIVATE_H__ +#define __SHELL_THEME_PRIVATE_H__ + +#include +#include "shell-theme.h" + +G_BEGIN_DECLS + +void _shell_theme_get_matched_properties (ShellTheme *theme, + ShellThemeNode *node, + CRDeclaration ***properties, + int *n_properties); + +/* Resolve an URL from the stylesheet to a filename */ +char *_shell_theme_resolve_url (ShellTheme *theme, + CRStyleSheet *base_stylesheet, + const char *url); + +G_END_DECLS + +#endif /* __SHELL_THEME_PRIVATE_H__ */ diff --git a/src/toolkit/shell-theme.c b/src/toolkit/shell-theme.c new file mode 100644 index 000000000..b64e15dff --- /dev/null +++ b/src/toolkit/shell-theme.c @@ -0,0 +1,1032 @@ +/* -*- 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 + * coelesce properties with the same name. + * + * In moving it to gnome-shell: + * - Renamed again to ShellTheme + * - 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 simplication + */ + +/* + * 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 "shell-theme-node.h" +#include "shell-theme-private.h" + +static GObject *shell_theme_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties); + +static void shell_theme_dispose (GObject *object); +static void shell_theme_finalize (GObject *object); +static void shell_theme_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec); +static void shell_theme_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec); + +#if 0 +enum +{ + LAST_SIGNAL +}; + +static int signals[LAST_SIGNAL]; +#endif + +struct _ShellTheme +{ + GObject parent; + + char *application_stylesheet; + char *default_stylesheet; + char *theme_stylesheet; + + GHashTable *stylesheets_by_filename; + GHashTable *filenames_by_stylesheet; + + CRCascade *cascade; +}; + +struct _ShellThemeClass +{ + GObjectClass parent_class; +}; + +enum +{ + PROP_0, + PROP_APPLICATION_STYLESHEET, + PROP_THEME_STYLESHEET, + PROP_DEFAULT_STYLESHEET +}; + +G_DEFINE_TYPE (ShellTheme, shell_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 shell_theme_init (ShellTheme * 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 +shell_theme_class_init (ShellThemeClass * klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructor = shell_theme_constructor; + object_class->dispose = shell_theme_dispose; + object_class->finalize = shell_theme_finalize; + object_class->set_property = shell_theme_set_property; + object_class->get_property = shell_theme_get_property; + + /** + * ShellTheme: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)); + + /** + * ShellTheme: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)); + + /** + * ShellTheme: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 (ShellTheme *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 * +shell_theme_constructor (GType type, + guint n_construct_properties, + GObjectConstructParam *construct_properties) +{ + GObject *object; + ShellTheme *theme; + CRStyleSheet *application_stylesheet; + CRStyleSheet *theme_stylesheet; + CRStyleSheet *default_stylesheet; + + object = (*G_OBJECT_CLASS (shell_theme_parent_class)->constructor) (type, + n_construct_properties, + construct_properties); + theme = SHELL_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 +shell_theme_dispose (GObject * object) +{ + /* ShellTheme *theme = SHELL_THEME(object); */ + + G_OBJECT_CLASS (shell_theme_parent_class)->dispose (object); +} + +static void +shell_theme_finalize (GObject * object) +{ + ShellTheme *theme = SHELL_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 (shell_theme_parent_class)->finalize (object); +} + +static void +shell_theme_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellTheme *theme = SHELL_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 +shell_theme_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellTheme *theme = SHELL_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; + } +} + +/** + * shell_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 + **/ +ShellTheme * +shell_theme_new (const char *application_stylesheet, + const char *theme_stylesheet, + const char *default_stylesheet) +{ + ShellTheme *theme = g_object_new (SHELL_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 (ShellTheme *a_this, + CRAdditionalSel *a_add_sel, + ShellThemeNode *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 = shell_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, + ShellThemeNode *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 = shell_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, + ShellThemeNode *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 = shell_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 (ShellTheme *a_this, + CRAdditionalSel *a_add_sel, + ShellThemeNode *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; +} + +/* This is a workaround for: + * - Python encodes '.' as '+' in type names + * - libcroco doesn't correctly parse the \+ escape in types + * - it's far too ugly to write foo\2b\bar\2bWidget... + * + * So we consider an element name of foo-bar-Widget to match a type + * of foo+bar+Widget + */ +static gboolean +element_name_matches_type_name (const char *element_name, + const char *type_name) +{ + const char *p = element_name; + const char *q = type_name; + + while (*p || *q) + { + if (*p == '-') + { + if (*q != '-' && *q != '+') + return FALSE; + } + else + { + if (*p != *q) + return FALSE; + } + + p++; + q++; + } + + return TRUE; +} + +static gboolean +element_name_matches_type (const char *element_name, + GType element_type) +{ + /* This is not efficient, but the number of selectors with an element_name + * is probably fairly small. + */ + if (element_type == G_TYPE_NONE) + { + return strcmp (element_name, "stage") == 0; + } + else + { + while (TRUE) + { + if (element_name_matches_type_name (element_name, g_type_name (element_type))) + return TRUE; + + if (element_type == G_TYPE_OBJECT) + return FALSE; + + element_type = g_type_parent (element_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 (ShellTheme *a_this, + CRSimpleSel *a_sel, + ShellThemeNode *a_node, + gboolean *a_result, + gboolean a_eval_sel_list_from_end, + gboolean a_recurse) +{ + CRSimpleSel *cur_sel = NULL; + ShellThemeNode *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 = shell_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 */ + { + ShellThemeNode *n = NULL; + + /* + *walk the element tree upward looking for a parent + *style that matches the preceding selector. + */ + for (n = shell_theme_node_get_parent (a_node); n; n = shell_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 = shell_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 = shell_theme_node_get_parent (cur_node); + if (!cur_node) + goto done; + cur_type = shell_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 (ShellTheme *a_this, + CRStyleSheet *a_nodesheet, + ShellThemeNode *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 = _shell_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 +_shell_theme_get_matched_properties (ShellTheme *theme, + ShellThemeNode *node, + CRDeclaration ***properties, + int *n_properties) +{ + enum CRStyleOrigin origin = 0; + CRStyleSheet *sheet = NULL; + GPtrArray *props = g_ptr_array_new (); + + g_return_if_fail (SHELL_IS_THEME (theme)); + g_return_if_fail (SHELL_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 * +_shell_theme_resolve_url (ShellTheme *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/toolkit/shell-theme.h b/src/toolkit/shell-theme.h new file mode 100644 index 000000000..e6f2897df --- /dev/null +++ b/src/toolkit/shell-theme.h @@ -0,0 +1,28 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_THEME_H__ +#define __SHELL_THEME_H__ + +#include + +#include "shell-theme-node.h" + +G_BEGIN_DECLS + +typedef struct _ShellThemeClass ShellThemeClass; + +#define SHELL_TYPE_THEME (shell_theme_get_type ()) +#define SHELL_THEME(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_THEME, ShellTheme)) +#define SHELL_THEME_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_THEME, ShellThemeClass)) +#define SHELL_IS_THEME(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_THEME)) +#define SHELL_IS_THEME_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_THEME)) +#define SHELL_THEME_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_THEME, ShellThemeClass)) + +GType shell_theme_get_type (void) G_GNUC_CONST; + +ShellTheme *shell_theme_new (const char *application_stylesheet, + const char *theme_stylesheet, + const char *default_stylesheet); + +G_END_DECLS + +#endif /* __SHELL_THEME_H__ */ diff --git a/src/toolkit/test-theme.c b/src/toolkit/test-theme.c new file mode 100644 index 000000000..f2bf3fcb1 --- /dev/null +++ b/src/toolkit/test-theme.c @@ -0,0 +1,292 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include +#include "shell-theme.h" +#include "shell-theme-context.h" +#include +#include + +static ShellThemeNode *root; +static ShellThemeNode *group1; +static ShellThemeNode *text1; +static ShellThemeNode *text2; +static ShellThemeNode *group2; +static ShellThemeNode *text3; +static ShellThemeNode *text4; +static ShellThemeNode *group3; +static ShellThemeNode *cairo_texture; +static gboolean fail; + +static const char *test; + +static void +assert_font (ShellThemeNode *node, + const char *node_description, + const char *expected) +{ + char *value = pango_font_description_to_string (shell_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 (ShellTextDecoration decoration) +{ + GString *result = g_string_new (NULL); + + if (decoration & SHELL_TEXT_DECORATION_UNDERLINE) + g_string_append(result, " underline"); + if (decoration & SHELL_TEXT_DECORATION_OVERLINE) + g_string_append(result, " overline"); + if (decoration & SHELL_TEXT_DECORATION_LINE_THROUGH) + g_string_append(result, " line_through"); + if (decoration & SHELL_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 (ShellThemeNode *node, + const char *node_description, + ShellTextDecoration expected) +{ + ShellTextDecoration value = shell_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 (ShellThemeNode *node, + const char *node_description, + guint32 expected) +{ + ClutterColor color; + shell_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 (ShellThemeNode *node, + const char *node_description, + guint32 expected) +{ + ClutterColor color; + shell_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 (ShellThemeNode *node, + const char *node_description, + const char *expected) +{ + const char *value = shell_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., + shell_theme_node_get_padding (group1, SHELL_SIDE_TOP)); + /* 12px == 12px */ + assert_length ("group1", "padding-right", 12., + shell_theme_node_get_padding (group1, SHELL_SIDE_RIGHT)); + /* 2em == 32px (with a 12pt font) */ + assert_length ("group1", "padding-bottom", 32., + shell_theme_node_get_padding (group1, SHELL_SIDE_BOTTOM)); + /* 1in == 72pt == 96px, at 96dpi */ + assert_length ("group1", "padding-left", 96., + shell_theme_node_get_padding (group1, SHELL_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., + shell_theme_node_get_padding (cairo_texture, SHELL_SIDE_TOP)); + /* From ClutterCairoTexture element selector */ + assert_length ("cairoTexture", "padding-right", 20., + shell_theme_node_get_padding (cairo_texture, SHELL_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", "toolkit/some-background.png"); + /* text1 inherits the background image but not the color */ + assert_background_color (text1, "text1", 0x00000000); + assert_background_image (text1, "text1", "toolkit/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", "toolkit/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", SHELL_TEXT_DECORATION_UNDERLINE); + /* :hover pseudo-class matches, but class doesn't match */ + assert_text_decoration (group3, "group3", 0); +} + +int +main (int argc, char **argv) +{ + ShellTheme *theme; + ShellThemeContext *context; + + clutter_init (&argc, &argv); + + theme = shell_theme_new ("toolkit/test-theme.css", + NULL, NULL); + + context = shell_theme_context_new (); + shell_theme_context_set_theme (context, theme); + shell_theme_context_set_resolution (context, 96.); + shell_theme_context_set_font (context, + pango_font_description_from_string ("sans-serif 12")); + + root = shell_theme_context_get_root_node (context); + group1 = shell_theme_node_new (context, root, NULL, + CLUTTER_TYPE_GROUP, "group1", NULL, NULL); + text1 = shell_theme_node_new (context, group1, NULL, + CLUTTER_TYPE_TEXT, "text1", "special-text", NULL); + text2 = shell_theme_node_new (context, group1, NULL, + CLUTTER_TYPE_TEXT, "text2", NULL, NULL); + group2 = shell_theme_node_new (context, root, NULL, + CLUTTER_TYPE_GROUP, "group2", NULL, NULL); + text3 = shell_theme_node_new (context, group2, NULL, + CLUTTER_TYPE_TEXT, "text3", NULL, NULL); + text4 = shell_theme_node_new (context, group2, NULL, + CLUTTER_TYPE_TEXT, "text4", NULL, "visited hover"); + group3 = shell_theme_node_new (context, group2, NULL, + CLUTTER_TYPE_GROUP, "group3", NULL, "hover"); + cairo_texture = shell_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/toolkit/test-theme.css b/src/toolkit/test-theme.css new file mode 100644 index 000000000..5776e0fe4 --- /dev/null +++ b/src/toolkit/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; +}