CSS styling in the Hippo Canvas



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

     HippoCanvasUrlLink
     HippoCanvasLink
     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.)

Attachment: signature.asc
Description: This is a digitally signed message part



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]