[PEAK] A layout and styling framework for UI components

Phillip J. Eby pje at telecommunity.com
Thu Dec 6 16:30:17 EST 2007


This is a rough draft of how I'll be implementing a new package 
called "Presentable", containing peak.ui.rendering, peak.ui.layout, 
and other modules.

The purpose of this package is to provide a basis for implementing 
and testing platform and GUI framework-independent presentation 
layouts, which are then transformed to specific widget sets, styles, 
colors, and spacing, using style sheets or "skins".

One of the main motivations for doing this is that creating wx widget 
layouts by hand becomes incredibly painful as the layouts get more 
complex.  There is a complex order in which the widgets must be 
created and initialized, plus a bewildering variety of options and 
special APIs needed to do adequate positioning and alignment.


Requirements
------------

One of the biggest complications involved is that GUI components must 
typically be initialized in a top-down fashion, beginning with a 
parent frame followed by its child controls.  However, in writing 
programs, it is much easier and clearer to specify objects in a 
bottom-up form, e.g.:

     Frame(children=[Control1(), Control2(), ...])

(Notice that an expression like this actually creates the parent 
*after* its children.)

So, one of the design goals for the layout framework is that you 
should be able to specify a presentation-component layout of 
arbitrary complexity using only a single expression, without needing 
to break it down into statements like this:

     frame = Frame()
     ctrl1 = Control1(frame)
     ctrl2 = Control2(frame)
     ...
     frame.children = [ctrl1, ctrl2, ...]

Of course, this means that the objects used to specify the layout 
cannot be the same objects that are used to *implement* the layout, 
since we can't make wx (or most other UI frameworks) play nicely with 
this approach.

But that's okay: for many things, that's what we want anyway.  It 
should be possible to take, for example, interaction model feature or 
scope objects, and treat them as inputs to the layout system.  For 
example, one should be able to lay out a bunch of interaction-model 
Field objects, or treat Scopes as panels in a frame.

In other words, the layout framework should allow you to use it as 
glue that wraps just about any sort of application object, and turn 
it into widgets of one kind or another, by applying style/skin information.

Further, there should be a way to easily specify horizontal or 
vertical concatenation of target objects, and this specification 
should *not* require any explicit spacing or positioning 
information.  Instead, it should be possible to have "layout hints" 
of some kind to determine the appropriate inter-component spacings.

In addition, it should eventually be possible to do mark-based row 
and column layouts (similar to the operations of Basser Lout), to 
automatically produce "grid bag" style constructs.

The framework should also include some standard classes for things 
like splitters, panels, frames, etc., and include a module that maps 
them to default wx implementations.  But none of these classes should 
depend on wx or the wx support module; instead, the default rules for 
a GUI framework should only be activated if the framework in question 
is imported.

The mapping from a set of abstract layout and interaction components 
to specific widgets and options should be done through a "skin" or 
"style sheet", and these skins or style sheets should be able to 
"inherit" settings from simpler skins or style sheets, all the way 
back to a default skin or sheet for the target GUI framework.  Skins 
or sheets should also be able to include conditional values based on 
the target platform, since wx often requires different flags or 
settings when running on different platforms.  They should also be 
able to handle the translation of abstract hints like "this is an 
alert" into what visual or behavioral properties an "alert" widget should have.

Finally, it should be noted that the primary focus of the framework 
will be for creating semi-"static" layouts - that is, ones where all 
the widgets to be created are known at creation time.

This does NOT mean that you won't be able to create highly dynamic 
layouts, including programmatically generated ones.  It's just that 
some features may only work well if the layout is known at rendering 
time.  You can, of course, still render sub-layouts and add them to 
an existing widget set if you know what you're doing.

To put it another way, widget rendering is generally not going to be 
a trellis-controlled activity, and widgets aren't necessarily going 
to be created or destroyed automatically when the data set from which 
they were built changes.  (The widgets themselves, of course, will 
respond to changes in the data models they're attached to.)


Tree Transformation
-------------------

So, we are basically looking at doing a type-agnostic tree 
transformation or "visitor" pattern, which is something that generic 
functions excel at.

If possible, I'd prefer to see if there's a way to use simplegeneric 
for this, without having to draw on the full power of a RuleDispatch 
or PEAK-Rules.  (This is more of a practical concern than a 
theoretical one -- I need the layout and interaction frameworks done 
*yesterday*, and PEAK-Rules is a few days work short of the necessary 
features.)

There are a few basic operations that the tree transformer needs to 
be able to do:

1. Determine what constructor and initial arguments to use to create the widget

2. Do any pre-child widget setup (e.g. setting wx properties and 
options that aren't specifiable via constructor)

3. Do any per-child operations  (e.g. adding to sizers, spacers, etc.)

4. Do any post-child setup  (e.g. calling Realize(), Layout(), Show(), etc.)

In principle, one could write this as four generic functions, each 
keyed off of the platform, GUI framework, skin, and item to be 
transformed.  But there are some complications, apart from the fact 
that we would prefer to avoid multiple dispatch for now.

Mainly, the complication is the fact that we want to allow multiple 
"hints" per layout item, and trying to make a generic function that 
dispatches on such arbitrary items is difficult.

So, instead of trying to combine methods, generic function-style, we 
need a way to combine the *results* of running various methods.  In 
other words, we need...


"Renderer" Objects
------------------

A renderer is a relatively simple object that exists to hold the 
state for, and co-ordinate the process of, rendering the presentation 
for an object.  It performs widget construction and 
pre/per/post-child processing, after first being "configured" for the 
object(s) being rendered.  It also holds references to the target 
object, the resulting rendering, and a parent renderer if 
any.  Renderers will also be able to "manage" context managers, such 
that the rendering process can be wrapped by context managers (such 
as the application of nested style sheets).

A renderer will also be the placeholder to which various AddOns can 
be added, each containing configuration data for the kind of 
rendering being produced.  These AddOns will usually be GUI 
framework-specific, in that addons for wx widgets will be distinct 
from those for Qt, etc.

Last, but not least, a renderer will possess "handler lists" for the 
four main operations of pre/post construction, and per/post child 
addition -- and an extra handler list for the process of *locating* 
the children to be added.  These handler lists will all have their 
contents called at the appropriate times.

Thus, the renderer type should be fully generic, as instances will be 
configured dynamically for the object being rendered.

But how?  Well, when a renderer is created, it will look up its 
target's type in a "skin" to find zero or more functions to call that 
will configure the renderer.  These functions will be able to add 
handlers or directly configure the renderer in any way desired.

So let's look at...


Skins and Style Sheets
----------------------

A "style sheet" is something that contains specifications for how 
arbitrary objects are converted into actual presentation (such as wx 
widgets and trellis-based event bridges).

Style sheets will be defined using class syntax, allowing the 
possibility of inheritance, e.g.:

     class wxStyles(DefaultStyles):
         """General styles for wx-based objects"""

     class MyAppStyles(DefaultStyles):
         """My application's generic style additions"""

     class MyAppInWX(MyAppStyles, wxStyles):
         """My application + wx"""

A "skin" is actually an *instance* of a stylesheet, and can contain 
mutable state information (although this isn't necessarily a good idea).

Skins can be dynamically "subskinned", to obtain a new skin that 
mixes the original skin's stylesheet with zero or more additional 
stylesheets.  For example, if you call ``.subskin(BarStyles)`` on an 
instance of ``FooStyles``, the returned skin will effectively be an 
instance of:

     class FooBarStyles(BarStyles, FooStyles):
         """Automatically-generated, merged stylesheet"""

and any instance attributes of the original skin will be copied into 
the new instance.

You will also be able to "update" existing skins with additional 
content, using the .update attribute, e.g.:

     class more_wx_styles(wxStyles.update):
         """Anything here will get added to wxStyles"""

The idea here is that when you define components that need default 
style information, you can just mix it in to an existing 
stylesheet.  (This prevents the need to mix a zillion stylesheets, or 
to have to have a different API for adding new rules to existing stylesheets.)

One caveat: you won't be allowed to define any non-rule attributes or 
methods in an ``.update``, as this could lead to conflicting or 
confusing results.  Attributes and named methods can only be 
introduced through normal inheritance in a new stylesheet.  (Oh, and 
you can only reference one ``.update`` as a base class - no multiple updates.)

So what are these "rule" methods?


Rules and Cascading
-------------------

A rule is just a method of a skin (stylesheet instance) that takes a 
renderer and target object as additional parameters.  The method is 
free to do whatever it wants to the renderer, such as add handlers, 
set up the constructor, or annotate it with configuration data and add-ons.

Rules will be registered in the body of stylesheet definitions, 
usually something like this:

     from peak.ui import rendering

     class MyAppStyles(rendering.DefaultStyles):
         """My application's generic style additions"""

         @rendering.rule(SomeApplicationType)
         def render_sometype(self, renderer, subject):
              # code to configure the renderer

All rules that apply to a given type are executed, in order of the 
target instance's MRO, then in order of the stylesheet's MRO.  So, 
for example, if you have this hierarchy:

      class Foo(object): pass
      class Bar(Foo): pass

      class MyStyles(rendering.DefaultStyles): pass

Then the order of rule precedence for a Bar instance is:

     * Rules defined in DefaultStyles[object]
     * Rules defined in MyStyles[object]
     * Rules defined in DefaultStyles[Foo]
     * Rules defined in MyStyles[Foo]
     * Rules defined in DefaultStyles[Bar]
     * Rules defined in MyStyles[Bar]

Note that more-specific rules are executed *later*, which thus gives 
them the opportunity to override things that are set up by a 
less-specific rule.  (Note: a given stylesheet may contain only one 
rule for a given target type, and once defined, it cannot be 
changed.  Among other things, this allows for caching.)

Also note that in the common case, rules are going to do nothing but 
add *other* functions to the renderer's handler lists.  So, we may 
need some other objects or decorators or convenience functions, to 
make this common task less repetitive.  However, what exactly that 
will look like is left unspecified for the moment, until we have a 
better idea of what's needed.


What's Left
-----------

This post doesn't actually cover how layouts will work, although it 
can be taken as a given that most layout requirements can be met 
through simple hierarchical construction and specifying of options on 
renderers.  I will probably post again at a later time regarding more 
sophisticated layout possibilities, but it will likely be after work 
on how to "hint" or "tag" interaction components (such as commands, 
fields, etc.) for use in rendering menus, toolbars, dialogs, and such.

Similarly, this post doesn't go into what sort of abstract layout 
components might be available by default, or what the standard way of 
"wiring" various interaction components would likely be.  There's a 
fair amount still to be accomplished in these areas, but I suspect 
that design work in these areas will be more productive if left until 
after the basic rendering framework is available.




More information about the PEAK mailing list