This chapter is divided into four sections. This chapter describes how the basic classes described in Chapter 3 are used and extended to provide a set of user interface elements. The objects presented here are intended to provide a basis for beginning to explore the system's potential, they do not exploit the full potential of a multi-scale user interface system. They include the set of familiar user interface objects - buttons, menus, and so on. The main goal of this chapter is to show that the mechanisms defined in the previous chapter yield implementations of conventional user interface elements that are simple and straightforward.
This chapter is divided into four sections. The first covers some simple visual elements such as backdrops, text labels, and frames. The next covers some basic event handler objects - one for dragging and zooming with the mouse, one with some bindings for changing the stacking order of objects, etc. The third section introduces some familiar interactive objects such as buttons and menus. The fourth section covers some basic multi-scale interactive elements.
A <label> is a single line of text whose font, foreground color, and background color can be selected. If the background color is set to boolean false (#f) the background will be transparent, whatever is below it will show through the UN-drawn areas of the text. This means the transparent? predicate must be defined to return the boolean negation of the background color, so that the system will always redraw the area below the label before drawing the label itself:
(define-method transparent? ((self <label>) (v <viewer>)) (not (get-background self)))
A <frame> is used to draw a visible border around an object. The border can be raised, flat, or sunken. It is implemented as a subclass of <group>, with a special render method that draws the border. It is frequently used to visually separate an object from its surroundings, or as part of an object such as a button that needs to switch from a raised to a flat appearance when activated.
A frame's thickness may be given using several different units of measure, either as a fraction of the frame's dimensions, or in terms of either the frame's public or private coordinate system. If given as a fraction, it can be either a fraction of the width, a fraction of the height, or else each thickness can be computed separately: horizontal as a fraction of the width and the vertical as a fraction of the height.
If the constructor is passed a tablet to be inserted into the frame then the frame's geometry will be adjusted so that the tablet just fits within the frame's inner border.
Besides <group>, the <frame> class has another superclass <frame-handler> which is an event handler to decide whether an event fell onto the frame's border or inside the frame. Based on this decision, it forwards these events to one of two handlers. The <frame-handler> also changes the cursor to a special shape when it is over the frame. An example of the use of a handler for events that fall on the frame's border is to drag and scale the frame and its contents.
One important type of tablet is the <image> class, which is a subclass of both the <tablet> class and the <pixmap> class. The <pixmap> class corresponds to a window system object containing a grid of pixel values. An image also has a path slot which holds the name of the file from which the image data is to be loaded. The <pixmap> constructor is able to recognize a variety of image formats by examining the file's header, and it applies various filters to convert the image to a common format. Finally, the image is loaded into memory and rasterized so that its pixel values are appropriate for the current window.
The image's private coordinate system is equal to its pixel coordinate system, so to render an area of an image we simply convert the area to integer coordinates and call the copy-shape method of the <pixmap> class. This method does a fast scaled copy of some sub-shape of the image to the window.
This section covers several objects derived from the <handler> class, which can be associated with a tablet to give it various types of useful interactive behavior. One of the most important features of the Tab API is the extent to which the interactive behavior is decoupled from the visual appearance. Each bit of interactive behavior is implemented in a different subclass of <handler>, and they can all be mixed and matched at will.
First we need to choose a set of bindings. We reserve mouse button one for functions specific to objects, or for popping up a menu if the event hits the background. Then we will assign
When the mouse comes up the view is updated and the timer is canceled. The zoom factor of the last step must be based on the time the mouse up event occurred, not the current time when the event gets handled. Even having taken these measures, the events can be delayed while they are waiting to be read from the socket connecting X server and client, and they aren't assigned a time until they are retrieved by the client side library. The only solutions to this problem are to keep the rendering from causing long delays by using semantic zooming, or to use multi-threading to retrieve the event asynchronously.
The zoom factor at any time is determined by the elapsed wall clock time that the mouse button has been down up to that point. By basing the zoom on the actual elapsed time we get a constant rate of zooming, even if sporadic delays occur. For a given zoom velocity v and time period t, the calculation is the same as figuring what interest rate is required to produce a given return - the desired zoom factor is f in the equation ft = v (as can be seen by setting t to either zero or one,) thus,
Other slots related to zooming include an action slot, which holds the symbol zooming while a zoom is occurring. This is used to decide whether a zoom needs to be canceled when the mouse button comes up. The slot fixed-point holds the mouse position at the time of the last timer event, and is used as the position around which the zooming occurs. The zoom-factor slot holds the amount by which the object is currently zoomed, and the zoom-velocity slot holds either zoom-in-velocity if we are zooming or its inverse if we are zooming out.
Note that the object being zoomed grabs the event stream during the zoom, so that the zoom continues even if the mouse happens to end up outside of the object's shape after a zoom step.
One more refinement that can be useful is to have the zoom start out at a slower rate to allow fine tuning of scale, later increasing to a higher rate for coarse adjustment.
Dragging is similar to zooming, but in some ways simpler. Like zooming, we save the initial mouse position as our fixed point. At any time, the position of the drag is given by the difference between the mouses current position and the fixed point. This avoids cumulative errors that might result from repeatedly adding incremental mouse movements to the object position. As in zooming, the event stream is grabbed during dragging, so that the mouse can move off screen without dropping the object if necessary.
There is also a subclass <tablet-bindings> of <drag-bindings>, which includes methods that are only appropriate for regular objects, not for cameras. These include an action to change the tablet to its natural size, and one to raise a tablet to just below the viewer. Note that these actions ignore the prefix, because they don't actually change the tablet's geometry, just the view or the stacking order. In order to save users the annoyance of getting lost, there is also a <home-handler> which is bound to the logo, and returns the main view to its original position when it receives a click event.
The <event> object has a method which returns a list of symbols that describe the event. This list starts with the most specific description (e.g. shift- buttonpress-2) and ends with the least specific (e.g. any-buttonpress.) To handle an event a search is made for a generic function that can handle the tablet that received the event as an argument. If no method is found in the most specific generic function, the less specific ones are tried. For example, the buttonpress-3 method of the <drag-bindings> handler starts a zoom out, so this method will be activated whenever a tablet that has <drag-bindings> as a superclass receives such an event, unless a more specific method exists in that generic function.
The <drag-bindings> object also has a prefix slot which holds a prefix which is prepended to all the event handling methods that it creates. This can be used to create a drag binder which only reacts when the shift- key or the control- key is down. By convention, actions that only affect the view are bound with no modifiers, actions that move or scale an object are bound with the shift- prefix, and objects that modify the view of a portal are bound with the control- prefix.
A button consists of a tablet, a frame around the tablet with some special bindings, and an action. The button is initially up, meaning that its frame is in ``raised'' mode. When a buttonpress or motion event with a mouse button down occurs inside the frame, the button goes into ``pressed'' mode, meaning that the frame takes on a ``sunken'' appearance. When a button release event occurs inside the frame the action is performed and the button goes back into raised mode. If the pointer leaves the button while it is down it goes up again without triggering the action.
Tab provides three classes derived from a basic <button> class:
2.02.0
A <menu> is a <group> containing several buttons arranged in a column or row. The menu constructor lays out the buttons in either a row or a column, and inserts them into the group beneath the camera associated with the menu. In vertical menus all the buttons are equal in both height and width, narrower buttons are extended to the width of the widest. Horizontal buttons are all the same height, but they are allowed to be of different widths.
A <popup-menu> is a <menu> which appears when an object (usually a <back drop>) which implements <popup-handler> receives a buttonpress-1 event. It then grabs the event stream and remains up until a buttonrelease-1 event is received. If that event occurs on one of the buttons it will be activated. A popup menu has a pop-marker slot, which holds the object below which the popup menu will be inserted.
When a menu is popped up, the position of the mouse and the scale of the view is saved. This position can then be retrieved from the menu to position objects created by an action in the menu. The menu is first positioned using the saved position and scale factors. Then it is moved so that the mouse is near the left side of the top button. (It is assumed here that the menu's orientation is vertical.)
The <tabview> is a subclass of the <viewer> object that
provides support for panning and zooming over the surface. It has
become traditional for a Pad system to provide such an interface for
navigating Pad space and the objects on it when the system is started,
and to provide a logo and other instruments that do not move - they
remain ``stuck to the glass'' of the user's monitor. This is
accomplished by creating an arrangement of portals and cameras which
are managed by a <tabview> object. This section describes how a
<tabview> object is initialized. The arrangement is shown in
figure 4.1.
The viewer is fixed - that is, it cannot be dragged around using the mouse. Below it is the logo, and below that a portal which is attached to a floating camera. The floating camera responds to various mouse events to allow the user pan and zoom around any objects between it and the backdrop. The <tabview> creates and manages this arrangement.
The next task is to begin designing applications that take advantage of Tabula Rasa's multi-scale features. Several other design issues can be identified:
We begin with a few simple (but useful) applications.
![]() |
A <magnifier> is a framed portal. The frame is given bindings (via inheritance) to allow dragging and scaling of both itself (via its border) and its look-on (by using the mouse while holding the control key.) The portal is connected to a camera which inherits from a constraint object to keep it centered on and directly below the frame, as shown in figure 4.4.1
A simple example of filtered rendering is a set of x,y data points which we wish to be able to see using different types of visualizations, such as a line graph or a bar graph. Suppose we have defined a class <xy-data> which can contain a set of data and which displays itself as two columns of numbers.
We will now define two subclasses of <filter> called <xy-linegraph-filter> and <xy-bargraph-filter>. When these filters are placed over an <xy-data> object we want them to give different views of the data. One of the filters will display the data as a line graph, the other as a bar graph.
The line graph and bar graph are constructed identically, except for the details of the render method. For the bar graph, a class <xy-bargraph-filter> is derived from <filter>. Its constructor initializes its delegate class to <xy-data> and its delegator class to <xy-bargraph>. The <xy-bargraph> class is derived from <xy-data>, and a render method for this class is created which scans the data to locate its extremes and draws the bar graph. That is all that is needed for a filter which simply changes the appearance of a tablet.
In the same way that the previous section was a demonstration of filtered rendering, this section is a demonstration of filtered event handling. A text buffer object is presented, and a filter which implements a simple work-through editor. When the filter is placed over a text buffer a new ``text being edited'' object is created whose type has a delegation style inheritance relationship with the text buffer's type. It extends the text buffer's functionality by adding a cursor and an event handler to interpret the various editing commands.
The <text> object is derived from <tablet>, and it contains all the information to describe the content and appearance of the text: a file name, the lines of text, a font, foreground and background colors, the widths of the margins and the aspect ratio of the page. It has a constructor method that reads the text from a file. It also has a render method that draws the text according to the appearance parameters described above. The render method is divided into two parts, a render-background method and a render-text method. This allows subclasses of <text> to use the text drawing method after drawing a different background. The <text> class also has methods to insert and delete text.
The <text-filter> object is a subclass of <filter> whose delegate class is <text> and whose delegator class is <text-being-edited>. The <text-being- edited> class is a delegation subclass of <text>. It adds two slots to the <text> class to hold the integers cursor-row and cursor-col, as well as a cursor-color slot.
The <text-being-edited> render method first fills the background with a slightly darker color than the <text> class background color, to visually distinguish editable text. It then determines the location of the character that the cursor is positioned over and fills the character's bounding rectangle with the cursor color. It then invokes the <text> method to draw the characters of the text.
The <text-being-edited> class also has methods up, down, left and right which move the cursor by damaging the old cursor location, changing the values of cursor-row and cursor-col, and then damaging the new position. Finally, the events are bound to actions by defining event-named methods for the cursor keys, the text keys and so on.
The next type of object is a filter for editing drawings, which are collections of drawing primitives. It demonstrates how controls can be attached to a filter by giving the user buttons to select the type of object to draw and the color to use to draw it. Because we need to associate some controls with the filter (to select the current drawing style and color), we derive it from a <group> rather than a <filter>. Inside the group go the control menus and a <filter>, to which we assign the delegate class <drawing> and the delegator class <drawing-being- edited>. A <drawing> is essentially a display list, while a <drawing-being- edited> adds to that some state variables to hold information about the object being drawn. For example, if we are drawing a rectangle the position of the initial corner is stored so a ``rubber band'' rectangle can be drawn until the final position is selected. At that point primitive <drawing> methods are called to add the rectangle to the display list data structure.
This section describes a tablet that provides a view of a directory tree. This is the first semantic zooming application, in that it displays a hierarchical data structure and allows the user to move closer and closer, revealing more detail in the process.
The viewer will lay out the elements of a directory in a rectangular grid, maintaining a certain aspect ratio and element spacing. Each member of the directory is represented by a separate tablet of a type suitable to the type of file it represents - images are represented by image tablets, text files by text tablets, and so on. In particular, each subdirectory is itself represented by the same type of directory viewer as the top level object.
Semantic zooming must be employed by the directory viewer to prevent any attempt to instantly load the entire content of each of its elements, which would cause a recursive loading of every file in the entire subtree. Instead, a directory is initially populated by <loader> objects, each of which are assigned to a particular path name and will load that object the first time it is displayed above a certain scale. Until that time they simply display a plain background with the name of the file they represent.
We also want the objects to disappear when they once again become small, as rendering even a small version of an object can be computationally expensive in relation to the amount of screen area they cover. For this reason we don't simply replace the loader object with the real object, but instead the real object is inserted into the loader, which is derived from the group class. The loader's render method checks the scale of any render requests and invokes its own simple rendering method at small scales and the default group rendering method at larger scales when we want to see the contained object.
We want the directory editor to be able to use standard Tab objects to represent directory elements - we don't want to invent special objects for the directory editor's use, or to make the directory a single object that has its own way of displaying files. For this reason, we derive the <directory> class from a <group> class, (initially <oblist>) and add a caption giving the folder's name. Then the elements representing the directory's files will be inserted into the group.
Now we need to determine a suitable lay out for the file nodes. Each file will have the aspect ratio given in page-aspect, and we want gutters between each row and column, but not around the edge. The vertical and horizontal gutters should be as nearly equal as possible. Our approach is to find the best layout with no gutters, and then to scale the nodes down until both vertical and horizontal gutters exceed a minimum size.
One approach is to try different numbers of rows until the amount of wasted space is minimized. Suppose we have n nodes to be laid out. We can compute the number of columns c for a given number of rows r as follows. We know that the first r-1 rows will each have c nodes, and the last will have between 1 and c-1, so


Which, for
, can be expressed as

Suppose now our desired aspect ratio (height divided by width) is a, and we have a rectangle of width w and height h. We need to choose the value for c which yields spaces in our layout whose aspect ratio is closest to a. For a given choice of c the actual aspect ratio of the spaces a' is


For a given choice of c, the resulting layout will either yield spaces for nodes which have too large an aspect ratio, in which case horizontal space will go unused, or to small an aspect ratio, in which case vertical space will go unfilled. As the number of columns increases, the aspect ratio will increase. We want to stop when the aspect ratio first exceeds the the target aspect ratio, and return either this or the previous layout, depending on which wasted less space. For an area w by h, the amount of wasted space will be

An adequate method of computing the best value of c is to try each value until the amount of wasted space begins to increase.
An <animation> is a tablet containing a vector of images, a current offset, and a frame rate. The constructor first checks that there are at least two images in the animation. It then locates the viewer argument so it can loop through images turning them from path names into <image> objects. Finally, it sets up timer loop by passing itself to set-timer and defining an alarm method which increments the frame number (modulo the number of frame), damages the animation, and re-sets the timer. The render method simply calls the render method of the current image.
This chapter has described the most basic Tab objects which have been implemented on top of the framework laid out in the previous chapter. With the creation of these objects we begin to explore the fundamentals of multi-scale application design.
Apart from their multi-scale behavior, the basic visual elements are similar to those in non-zooming systems: text, images, frames, etc. Interactive behavior is assigned by mixing event handling super classes into an object using multiple inheritance. Timer events are used to achieve animated effects, zooming in and out in particular. Filtered rendering and filtered event handling provide a means of communication between objects.
An examination of the implementation of Tabula Rasa will show that each of these objects are implemented quite simply on top of the framework provided in chapter 3.