CSS in the Hippo Canvas

This page was originally posted here.

Motivation

We added some simple theming support to the Online Desktop sidebar a while ago. The way it worked is that there was a global ThemeManager object with a get_current_theme() method, and a set of "themed" subclasses of the different Canvas objects that got colors from the theme object (so, ThemedText, ThemedLink, etc.)

There were a couple of problems with this: one is that having these subclasses was ugly and inconvenient. A second problem was that we didn't actually want consistent theming in all places: the online desktop sidebar is themed, but the same UI elements could appear in a "browser" window that was meant to match the system appearance.

So, the same PersonItem would need ThemedLink objects in one context, but in another context, would need plain old hippo.CanvasLink objects. Trying to make this work meant threading a 'themed' boolean through all our construction code and lots of conditionalization.

This idea of having the same objects appear differently in different contexts is something we've hit before. A particularly bad example was the appearance of "hushed" blocks in the Mugshot stacker. When you "hushed" a block, all the links in the block are supposed to appear hushed. Implementing this required:

  • Propagating the 'hushed' status down to all the descendant elements within the block (so things like the quip preview have this as a property.)
  • Adding code in each of them to adjust the appearance of the links when the hushed status changed.

Now, of course, controlling appearance in a way that depends on context is exactly what you do with CSS for a web page. Rather than adding something that was "like CSS" but different, I thought I'd try to literally use CSS stylesheets using libcroco to do the parsing.

I have this working pretty well now and have ported both the online desktop sidebar (in Python) and the Mugshot client (in C) to work with the CSS-ified canvas.

Basic API

The basic API for this is pretty simple:

/* Any parameter can be NULL */
HippoCanvasTheme *hippo_canvas_theme_new (HippoCanvasThemeEngine *theme_engine,
                                          const char             *application_stylesheet,
                                          const char             *theme_stylesheet,
                                          const char             *default_stylesheet);

void hippo_canvas_set_theme (HippoCanvas      *canvas,
                             HippoCanvasTheme *theme);

void hippo_canvas_window_set_theme(HippoCanvasWindow *canvas_window,
                                   HippoCanvasTheme  *theme);

So, you can set a stylesheet for a canvas, and you can have a cascade of stylesheets:

  • application_stylesheet: styling for the application ... highest priority
  • theme_stylesheet: styling for the "theme" ... middle priority
  • default_stylesheet: "default" stylesheet ... lowest priority

To specify how selectors work on the items HippoCanvasBox also gains a couple of extra properties:

  • "id": the ID for CSS matching
  • "classes": the class (or space separated classes) for CSS matching

"classes" is not "class", because that would cause a problem in Python where 'class' is a keyword - you couldn't do:

 text_item = hippo.CanvasText(class='highlighted', text="foo")

Selectors

Much of the CSS 2.1 selectors specification works. You can match on element name - the way this works is that you can match on *any* objects type in the HippoCanvasItem's type hierarchy, so a HippoCanvasUrlLink will match

 * HippoCanvasText  * HippoCanvasBox

To match on a Python type, you replace the .'s in the module name with -'s, so bigboard.big_widgets.Separator matches:

  bigboard-big_widgets-Separator

You can match on element ID and class.

The :link and :visited pseudo-classes are supported (out of the box for HippoCanvasLink subclasses)

You can use the '>' combinator

Some things aren't:

  • Adjacent sibling selectors ('+') are not supported
  • Attribute selectors ('[att=val]', etc) aren't supported
  • :first-line, :first-element, :before, :after are not supported

Properties

Some of the CSS properties that are supported by the standard items in my current patch:

  • border[-width,-color][-left,right,...]
  • padding[-left,right,...]
  • color
  • background-color
  • text-decoration
  • font[-style,-family,-weight,-variant,-size]

I've attempted to follow the actual CSS specified behaviors pretty closely, though there are deviations both from what's possible without major changes to the canvas, and what I just didn't bother to get 101% right. (As an example, a compliant CSS parser should reject and ignore 'border: 1px solid solid black', but I treat it the same as 'border: 1px solid black'.)

I'm not going to list all the CSS properties that aren't supported - that would be a looong list. Speaking generally, layout properties like display, float, left/right/top/bottom don't make sense to me. The HippoCanvas deviates substantially from the CSS box model and this (and the ability to write custom layout managers) are one of the big reasons you'd use it instead of a HTML widget.

Another thing that generally isn't supported is percentage values for lengths - you can do 'border: 2px' or 'border: 2em', but you can't do 'border: 25%'. (Percentage values for lengths that affect layout are inherently circular and anybody who has done significant work with CSS on the web will have had lots of fun with their oddities and traps.)

"Theme engines"

You'll note the "theme_engine" parameter above; the idea of a theme engine is that it is an object that knows how to draw named objects. The HippoCanvasThemeEngine has a single parameter:

 gboolean (* paint) (HippoCanvasThemeEngine *engine,
                     HippoCanvasStyle       *style,
                     cairo_t                *cr,
                     const char             *name,
                     double                  x,
                     double                  y,
                     double                  width,
                     double                  height);

This is manifestly *not* enough to do a full widget theme (with things like notebook tabs), but it handles the simple stuff that we do for the Online Desktop sidebar.

Implementation Notes

The way I did things was rather than to have CSS matching be done directly on the tree of HippoCanvasBox, was to create a parallel tree of HippoCanvasStyle objects.

This has several advantages:

  • It doesn't tie things directly to HippoCanvasBox which is, after all, supposed to be only one implementation of HippoCanvasItem.

  • It avoids making hippo-canvas-box.c yet longer and more complicated.
  • We don't have to worry about style mutation - if the element ID or class of an object changes, we just drop the style object and recreate a new one on demand.

My initial plan was to make HippoCanvasStyle general - to have API like:

gboolean hippo_canvas_style_get_length (HippoCanvasStyle *style,
                                        const char       *property_name,
                                        gboolean          inherit,
                                        gdouble          *length);

But the desire to actually match normal CSS made this work out not so well. In this framework, how do you handle the interaction of:

 border-width:
 border-left-width:
 border: [ <border-width> || <border-style> || <'border-top-color'> ] | inherit
 border-left: [ <border-width> || <border-style> || <'border-top-color'> ] | inherit

You need to know what order they appear in the style rules. So, I ended up adding more specific API like:

 double hippo_canvas_style_get_border_width (HippoCanvasStyle *style,
                                             HippoCanvasSide   side);

Being specific also makes it easier to cache property values instead of computing them over and over again. (For this reason, some things that are currently handled generically like the 'color' property for foreground text color probably should be switched to being hard-coded.)

Concerns

My concerns about this change (especially in the context of usage of HippoCanvas in OLPC/Sugar) are two-fold: compability, and performance issues. (And among performance I include memory usage.)

I've tried fairly hard to preserve compatibility with this change - I've left almost all the individual item object properties the same. But there are a couple of incompatible changes:

  • The color-cascade property is gone. What this property was about was whether colors where inherited (not "cascaded") from parent items is gone.
  • Font and color set by object properties are now never inherited by child widgets. (Previously, fonts were always inherited, colors inherited depending on the color-cascade property.)
  • The ability to set the default color for an item type by modifying a class property, and the ability to modify the background color for an item by setting the background_rgba field directly are gone. Neither of these was bound into Python so there is likely no effect on the Sugar usage of the canvas.

It didn't take me very long to fix up the Mugshot stacker to deal with these changes, but the lack of inherited color does mean that you'll have to add a stylesheet in some cases where one wasn't used before.

(Digression: why did I decide to make font and colors set through GObject properties not inherited? I had to decide one way or the other since 'color-cascade' was (IMO) a bad idea, and non-inherited was simpler. Keeping object property styles as a layer on top of the CSS styles also allowed doing something like adding a custom style property -hippo-mouseover-color: and use that modify the color object property.)

There is obvious CPU overhead both to match CSS styles and memory overhead to keep the HippoCanvasStyle objects around. The CPU overhead should be basically zero if you don't have any stylesheets or style rules, but the memory overhead doesn't .. right now it is ~150 bytes per canvas item.

The bulk of this is:

  • A copy of the font description
  • The border-width/-color/padding properties for each side of the item

150 bytes per item isn't horrible - even for a 1000 item canvas, that's only 150k, but it certainly is noticeable. Possible improvements:

  • Storing border/padding as a double is really wasteful. guint8 is probably fine. (You don't want non-integral margin/padding since it will give you blurred lines and blurred child elements)

The style of an object is determined by:

  • The matched rules
  • The parent style

We could conceivably hash those and have only one copy of the HippoCanvasStyle object for each combination. When you have a canvas with a lot of objects, you'll probably have a high hit rate for this. This would also save CPU since while the rule matching would have to done for each element, converting from a list of matched rules to a set of properties only needs to be done once.

TODO / Refinements

Much of HippoCanvasStyle is not bound into Python. Partly this is because I'm less certain of the HippoCanvasStyle API, partly because I didn't need things like hippo_canvas_style_get_border(style, side) in the Python code of the online desktop sidebar.

As mentioned above, I want to move color and background-color to be hard-coded in the HippoCanvasStyle API instead of going through a generic get_color().

Some of the generic API of HippoCanvasStyle (get_length()/get_side()/get_enum()) probably should be eliminated.

It would be handy to allow setting a CSS fragment as an object property similar to the HTML CSS attribute so you could do

    hippo.CanvasText(style="text-decoration: line-through;")

I'm not sure if libcroco exports a suitable API to allow parsing a fragment like this.

The 'id' and 'classes' properties perhaps should be moved to HippoCanvasItem instead of HippoCanvasBox ... in the theory that a non-HippoCanvasBox should still support this properties.

Having hippo_canvas_box_add_class()/remove_class() convenience functions would be useful.

I'd like to support RGBA colors through the style file, but the CSS3 syntax of rgba(...) is not supported by libcroco, so this would require a cut-and-paste of libcroco or getting a patch upstream.

(cut-and-pasting libcroco has some appeal, in that it allows getting rid of the useless libxml dependency, but is clearly community-unfriendly.)

The way that you support :link and :visited is really ugly: you override hippo_canvas_context_get_style() and do something like:

static HippoCanvasStyle *
hippo_canvas_link_get_style(HippoCanvasContext *context)
{
    HippoCanvasBox *box = HIPPO_CANVAS_BOX(context);
    HippoCanvasLink *link = HIPPO_CANVAS_LINK(context);

    if (box->style == NULL) {
        context_parent_class->get_style(context);
        hippo_canvas_style_set_link_type(box->style,
                                         link->visited ?
                                              HIPPO_CANVAS_LINK_VISITED :
                                              HIPPO_CANVAS_LINK_LINK);
    }

    return box->style;

}

Probably should just add a "protected" hippo_canvas_box_set_link_type(). This does mean that if more things like hippo_canvas_style_set_link_type() they would also need to be added to HippoCanvasBox, but I don't see a need to keep on extending that set.

The patch

So I don't get accused of vaporware:

 http://www.gnome.org/~otaylor/hippo-canvas-css.patch

A little more raw:

 http://www.gnome.org/~otaylor/mugshot-canvas-css.patch
 http://www.gnome.org/~otaylor/bigboard-canvas-css.patch

Appendix: So why not just use an HTML widget?

Once you start adding CSS to HippoCanvas, the obvious question becomes "OK, why aren't you just using WebKit or GtkMozEmbed or something. There are a couple of reasons I see hippo.Canvas as still better for certain purposes:

Subclassing: When writing the Mugshot stacker and the Online desktop sidebar it has worked out really really well to be able to subclass standard HippoCanvas items and bind them to data, whether it's an item for a headshot of Mugshot person or group, or a timestamp item that automatically updates from "1 minute ago" to "1 hour ago".

This is not something you get out-of-the box with an HTML widget: a HTML DOM tree is strictly generic. There are of course Javascript widget systems, and there is XUL, but (with certain complex exceptions like GWT) Javascript widget systems are generally speaking hack-jobs, and XUL is *not* a HTML widget. It's a different toolkit.

Subclassing in HippoCanvas + python is just a really nice way to create items with custom appearance and behavior with minimal code.

In your language: While it's conceivably possible to use other languages than Javascript within a HTML widget, the web ecosystem is a Javascript ecosystem and doing something else is asking for pain.

(And by use, I don't just mean a bit of manipulation of the DOM tree, I mean establishing event handlers, and so forth.)

And integrating canvas items in Javascript into a program written in another language means that you have a language barrier to fight that you have to cross with XPCOM or whatever.

Better layout: The CSS layout model is flexible and powerful, but it is really designed for incremental layout of text, not squeezing the most information into a small space. HippoCanvas has box layout, it has ellipsization.

Ability to write custom layout managers: doing something like a icon list that wraps to a given width in Javascript is horrible and almost always a bad idea... after all, you are fighting another layout system built into the HTML engine. With HippoCanvas you can participate in the layout system as a real peer.

Rich GTK+ integration: you can embed HTML widgets into GTK+ several ways, but I don't know any provisions for embedding GTK+ widgets into HTML.

(Of course, in other cases using a HTML widget *is* the right way to go.)

Projects/HippoCanvas/CSS (last edited 2013-11-22 23:25:42 by WilliamJonMcCann)