Drawing code

In Slice, the state tree completely determines the "semantics" of the application. Specifically, this has two parts: (1) how the display appears, and (2) the source node for each event (that is, for each event, the node from which the scripts to be executed are obtained).

The implementation of Slice must respect the semantics given by the tree (which we outline below). We will see that the semantics is fairly straightforward, and that an easy way to do this would be to redraw the tree, from the root, each time the tree changed. But this would be inefficient and would result in an unacceptable amount of flicker. The actual implementation emulates the effect of redrawing the tree, but more efficiently and with less flicker.

This document explains the semantics of the tree, giving a somewhat more formal version of the documentation. It then describes the specific goals of the implementation, the private fields of the state tree (whose purpose is to support efficient redrawing), and the redrawing code itself.

Semantics of the state tree

The general outline of how trees are drawn was given in the high-level overview. Here we are more formal, giving pseudo-code to describe how nodes are drawn "from scratch" (which, in fact, is exactly what happens when Slice is started and an initial tree is read in). The implementation is intended to emulate the effect of redrawing from scratch whenever necessary, without actually doing it.

First we discuss drawing and then the determination of source nodes.

Drawing

Drawing starts at the root node, which can contain scripts and pen attributes. It passes this information to its Frames child, of which it must have exactly one. The Frames node can also have script and pen attribute information, which it passes to each Frame child. The Frame nodes have sizes (X, Y, W, H), and each Frame child must have a Visible attribute that is either True or False. Frame nodes with Visible = False are ignored. The visible Frame nodes must have a DisplayId attribute; say its value is id. There must be a node somewhere in the tree whose Id attribute is id; call it displayedNode. displayedNode must be either a Panel or InkPanel node. We then call the Draw function on displayedNode, providing as arguments the pen attributes, script attributes, and location/size attributes we have gathered.

This brings us into the main part of the drawing code. Each node is drawn within the coordinate system (bounding box) given by its parent. Note that the boundingbox is in pixel coordinates. If a node has Visible = False, then neither it nor its descendants are drawn.

// Draw visible nodes
Draw (node, scripts, penattrs, boundingbox):
   Add scripts and pen attributes given in node, if any, to scripts and penattrs
   Calculate new boundingbox from boundingbox and X, Y, W, H attributes
     of this node, using default values for any missing attributes
     (0 for X and Y, inherited values for W and H)
   switch(name of node):
     Panel, InkPanel, Button: Allocate object of the given kinds, with
       size given by new bounding box, and associate it with this node.
     Stroke: Strokes always have X, Y, W, and H coordinates, and floats
       field gives stroke points in range [0,1], which are scaled to bounding
       box of the stroke. Use pen attributes either given in the node
       or taken from penattrs; missing attributes are put into the node.
     (All objects created in the preceding switch statement are placed within the
     containing object at the location and size given by the new bounding box.)
   for each child:
     Draw(child, updated scripts, updated penattrs, new bounding box)

Source nodes

The source for each event depends upon the event type, for example:

  • OnClick: The node whose associated Button object was clicked
  • StrokeAdded: The InkPanel on which the stroke was entered.

The source node for each type of event is given in the scripting guide.

The event type and source node together determine the scripts(s) that are executed for an event. From the traversal described above, each node has a list of scripts for each event type (the ones passed into the node plus the ones added by the node, if any). Those are the scripts executed when this is the source node for an event of a given type. They are executed in order of lowest node first, as described in the documentation

Implementation

The implementation must emulate the effect of drawing the tree using the above algorithm. The basic idea is to respond "minimally" to every change in the tree.

(Terminology: Nodes that are visited during the drawing traversal - i.e. those that appear on the screen - are called visible. Other nodes are called hidden. Note that if a node is hidden, then all its descendants are also hidden. Also, any subtree that is not attached to the state tree is hidden.)

The goals of this code are:

Ease of scripting: Scripters should not have to be concerned about the details of redrawing, but should be concerned only with modifying the tree correctly for their application.

Appearance: Flicker should be minimized.

Efficiency: Aside from the desire for overall efficiency, we have two specific goals:

  • Re-allocation of objects and, especially, recalculation of stroke points, should be avoided whenever possible.
  • Traversal of hidden subtrees should be avoided. The visible portions of the tree are inherently of limited size (limited by the amount of information that can be fit on a screen), while the hidden portions - such as the lecture slides other than the current one - can be enormous.

Redrawing from scratch cannot satisfy the appearance or efficiency goals. Instead, the implementation adjusts the display each time a change is made that might affect it. When such a change is made, the code traverses only that part of the tree that is affected by the change, and does not redraw the entire tree.

To do this, we need to keep track of every change that might affect the display. These changes are:

  • Add a new subtree to a visible node (assuming the new subtree does not have attribute Visible = True). Note that the new subtree was necessarily not visible, since it had no parent.)
  • Remove a visible subtree. By definition, the node and all its descendants become hidden.
  • Change certain attributes of visible nodes:* The Visible attribute changing from True to False or False to True.
    • The DisplayId of a Frame node
    • "Heritable" attributes - script and pen attributes, and size attributes (X, Y, W, H)
    • "Personal" attributes - such as BackColor or Text - being altered.
  • Remove certain attributes:
    • Removing the Visible attribute, if its value was False and its parent node is visible
    • Removing a heritable attribute, thereby allowing its value to be inherited from an ancestor
    • Removing a personal atrribute, allowing it to assume a default value

Note that it is not legal to remove the DisplayId node of a Frame node, since this would leave it (even if temporarily) without a DisplayId value. Similarly, it is not legal to change the Id attribute of a node that is pointed to by a Frame node, because this would again leave the Frame node pointing to nothing (unless there were two nodes with the same Id, which is also illegal).

Redrawing algorithm

Tree nodes have private fields which are used to preserve information needed to redraw efficiently. When a node is visible, these fields are known to be correct; when it is hidden, they are not (with some exceptions).

visible: Indicates whether the node is visible.

theObject: The control or stroke objects created by the node, if any.

container: The object into which this node will place any objects it creates; or, for container nodes - Panel, InkPanel, Frame, InkTextBox - the object created by this node.

bbox: Bounding box that represents the coordinate system used by the children of this node. For non-container nodes, this is the size and location, in pixels, of the node. For container nodes, it gives the size of the node, in pixels, but the location is (0,0). (For InkTextBox nodes, it gives the size of the underlying text area.)

locationInParent: For container nodes, the location within the parent (since bbox is set to (0,0)).

penattrs: TreeNode giving the nearest enclosing PenAttributes node (or the PenAttributes node that is a child of this one).

scriptlist: Scripts at this node - inherited node plus scripts defined at this node

Invariants

  1. No hidden node can have a visible child. Thus, every visible node has a visible parent (in the sense of the traversal) except the root node (which has no parent). The root node and its Frames child are always visible and cannot be hidden, even by setting the Visible attribute to False. There is only one root node. All disconnected nodes are hidden.
  2. If a node has an associated object, whether or not the node is visible, the fields theObject, bbox, locInParent, and container match what is in the object. That is, the properties of the object and these private fields change in synchrony. (This is not to say these fields match what is in the tree; if the node is hidden, its attributes may have changed but would not be reflected in the object or these private fields.)

Redrawing operations

When the tree changes in one of the ways described above, a redrawing operation is invoked. These are the operations:

  • Draw: Called when a node becomes visible, either by becoming the child of a visible node, by having a Frame node's DisplayId changed to this node's Id, by having its Visible attribute changed from False to True (when its parent is visible), or having its Visible attribute removed (when its value was False and its parent is visible). Draw is only called on hidden nodes.
  • Hide: Called when a visible node becomes hidden, either by being removed from its parent, by having its Visible attribute changed to False, or by having a Frame node's DisplayId changed when its previous value was this node's Id. Hide is only called on visible nodes.
  • PropagateX, where X is PenAttributes, Script or Size. These operations are only applied to visible nodes. Specifically:

    • PropagatePenAttributes: Traverse, making required change in any ink overlays, until reaching a node with its own PenAttributes child (or an invisible node).
    • PropagateScripts: Traverse all visible nodes, recalculating the scripts
    • PropagateSizeChange: Traverse, making required changes, until reaching a node whose bbox is the same as the new bbox (being careful of InkTextBoxes and containers). Note that if any *visible* descendant of sucha node had its size attributes changed, there would have been handled previously. If any *visible* descendants were changed, it doesn't matter: they will be drawn correctly whenever they become visible again.

The key questions here are:

  1. When can Draw reuse the object in a node?
  2. Assuming the object in a node is reused, can it be used unaltered? We divide this into two questions:
    1. When can Draw reuse the object without any alteration?
    2. When can Draw reuse the object without changing its size and location?
  3. When should Hide destroy an object and clear the private fields of a node?

Here are the answers we give in this implementation:

  1. The object is reused as long as the container in which the object was originally created is the same as the one in which it is being placed. (Or: Strokes are reuse when they are placed in the same ink overlay; other objects are always reused.)
    1. Never. Personal and heritable atrributes, other than size attributes, are always reloaded. (The reason for this is that keeping track of which ones have changes since the noe was last visible would take more work than reloading.)
    2. If the existing bbox and locInParent match their new values, there is no need to recalculate. (This check is worth making mainly because it allows us to avoid recalculating points in strokes.)
  2. Never.
    Last updated on Wed Aug 17 22:30:47 CDT 2005 .