Skip to content

Instantly share code, notes, and snippets.

@ialexi
Created October 5, 2010 16:03
Show Gist options
  • Save ialexi/611794 to your computer and use it in GitHub Desktop.
Save ialexi/611794 to your computer and use it in GitHub Desktop.

SC Rendering Architecture

This is a bit wordy, because it actually goes into detail on how the implementation would work, down to how it would approach reordering child views in DOM and the drawbacks of that method.

The summary and examples cover the effects at a higher level.

Goals

  • Where possible, maintain backwards-compatibility--but not at the expense of cleanliness of design.
    • This design is actually more compatible with 1.4 than 1.5, as certain elements (render function) will no longer be deprecated, even for views which otherwise would use renderers.
    • However, other parts may break backwards-compatibility; this design gets rid of prepareContext, renderLayout, etc., so views that override them will break.
  • Make Renderers easy.
    • Creating renderers should be really easy. Right now, it is a bit of a pain. Views should not be limited to one renderer, either.
  • Separate rendering logic from layer logic.
    • That is, rendering logic, except at low levels, should have no knowledge of DOM. Eventually we will change our event logic to be able to work the same way (eg. allowing hit-testing, etc.).
  • Separate Views from DOM entirely.
  • Get rid of all of the in-View styling (textAlign, font boldness, etc.)
  • Make it really easy to write themes. See Chance 2.0 document.

Log Line

Views, rather than maintaining DOM, maintain a "Render Tree" that syncs with DOM at the end of the run loop.

Summary

The goal is to render to Layers. Views have no idea how to do this, as layers are really just very small wrappers around native elements such as DOM elements (DOMLayer is such a wrapper).

The View tree, as such, maintains a Render Tree, which is a tree of objects that do know how to actually update the layers. This Render Tree consists of RenderContexts, which the view creates by calling layer.createContext(). Each RenderContext may have child render contexts (perhaps contexts created by child views of the view that created the context), and this forms a tree.

Although it is not obvious at first, render() almost never actually does any rendering or updating directly; instead, render() maintains the render tree. However, the commands that do this look 100% like actual rendering commands.

As such, THE RENDER TREE IS LARGELY TRANSPARENT AND AUTOMATIC. To most people, it looks like render() actually just renders, and that's all, but under the hood, you are creating a tree of RenderContexts. One could call this a behind-the-scenes optimization that one doesn't need to care about unless they are a framework developer.

The View Tree (consisting of Renderers and Views) owns layers, and creates and maintains a Render Tree which it uses to update those layers.

Theming is central to this design: the theme is who tells the views what type of layers they should create. RenderContexts are attached to themes (and would likely be recreated if the view's theme changes). The RenderContext is usually used to create renderers (these renderers, while managed by the RenderContext for the view's convenience, are still part of the view). The RenderContext base class handles all of this.

CSS is made much simpler by making the various low-level renderers know exactly how to talk with CSS (and, more important, with CSS extensions made by Chance). This especially applies to the handling of images.

A button renderer before this redesign would be forced to create DOM elements for each slice, provide class names for each, etc. In the new architecture, the renderer simply asks for some slices to be drawn, gives a single name identifying that set of slices ("button", for instance), and it is done. Implementation of those slices is handled for it (whether it uses data: urls or anything else).

The Chance extensions discussed in the Chance 2.0 document cover how it integrates with these changes.

Finally, how themes manage renderers will now be the responsibility of the theme. When a context needs to find a renderer, it will call the theme's getRenderer method, which will return a renderer instance.

Examples

Because everyone loves examples.

Rendering and theming are much improved:

SC.ButtonView = SC.View.extend({
  theme: 'capsule',
  render: function(context) {
    context.draw(SC.BUTTON_RENDERER, { 'title': 'My Button' });
  }
})

Button Renderer (part of the theme, created by .render above):

MyTheme.renderers.button = SC.Renderer.extend({
  render: function(context) {
    context.draw(SC.SLICES_RENDERER, 'button');
    context.draw(SC.TEXT_RENDERER, this.title);
  }
});

And the CSS (roughly; some still undecided):

theme.button {
  slices('button.png', $left: 8, $right: 8, $slice-padding: 5 );
}

theme.button.active {
  slices('button-active.png', $left: 8, $right: 8, $slice-padding: 5 )
}

@theme(capsule)
  theme.button {
    slices('button-capsule.png', $left: 8; $right: 8; $slice-padding: 5 );
  }
@end

View Layer

View Tree

The View Tree consists entirely of Renderers. Some items in the view tree own layers (all views own their own layers); others, such as Renderers, do not.

Views create their layers by using their theme. The view then calls layer.createContext() to get a starting RenderContext for the layer, which it then gets added to its parent view's RenderContext. This, in essence, adds the child view to the rendering tree.

Layers

Layers represent a surface. You can think of them like a layer in Photoshop: you can move them, you can change their opacity, you can change their contents.

While this plan only addresses DOM-based layers, it is possible to have canvas-based layers. Note that in such a case, each canvas would not be a layer; rather, there could be several "virtual" layers inside the canvas; all of the layers, however, would point to the same canvas. The point of this is that the basic functionality of moving a layer around and such is very central to how apps work.

Layers are SC.Objects. They have "layer properties" which affect the layer as a whole. Note that not all layer properties work on all platforms or even all types of layer.

Readable:

  • supportsAcceleratedCompositing
  • supportsOpacity
  • supportsTransforms
    • determines if roatate, scale are supported.
  • supports3DTransforms
    • in all current browsers, if supportsAcceleratedCompositing is true, this will be as well.

Writable:

  • layerId. Event system will be expected to find a layer with an id that can be used to identify a view.
  • left
  • top
  • width
  • height
  • right
  • bottom
  • depth
    • for a SC.DOMLayer, this would map to zIndex unless use3DTransforms is YES.
  • opacity
    • may not function on IE < 9.0
  • rotate, rotateX, rotateY, rotateZ
    • 2D support on some platforms; 3d only on ones with 3D transforms.
  • scale, scaleX, scaleY, scaleZ
    • 2D support on some platforms; 3d only on ones with 3D transforms.
  • useAcceleratedCompositing
    • can be set to YES even if not supported; it will be ignored.
  • use3DTransforms
    • if set to YES, useAcceleratedCompositing is assumed.

Types:

  • Layer
  • DOMLayer (extends Layer)

RenderContext

RenderContexts know how to update a layer's contents. Contexts are created by views or other contexts; to do so, views call:

  • layer.createContext(theme)
    • note: layers and contexts do not actually know of themes; however, themes are Renderer Providers-- that is, they implement getRenderer(name), and that is what createContext expects.

RenderContexts call the same method to create more RenderContexts.

RenderContexts have a few properties:

  • layer: The layer the RenderContext should render to. Multiple render contexts can (and often will) share the same layer.
  • renderer: may or may not be present; used to make the context "intelligent".
  • parentContext: The parent RenderContext in the render tree.
  • childContexts: Any child RenderContexts in the tree.

RenderContexts have some higher-level commands:

  • render(renderer, args...)
  • clear()
  • lock()

Each of these simply instantiates a renderer from the provider supplied to the context. While technically the renderers themselves are still part of the view tree, the RenderContext will manage them for the view's convenience; for sake of organization, it will give these renderers their own RenderContexts, and make these part of the render tree.

Calling .clear() will clear all child contexts. Also, the context can be "locked" so that any updates to layer content will call clear() implicitly; this is done before any call to a view's or a renderer's render().

Further, subclasses of RenderContext that are related specifically to types of layers will have their own extensions, most of which will be rather low-level, and only the lowest-level renderers should use.

For example, DOMRenderContext has methos like push, begin, and end.

Intelligence

RenderContexts are intelligent.

While clear() will implicitly be called if drawing has already occurred once and render() gets called again, RenderContexts notice if you render the same things as before. With few exceptions, the context will not be entirely re-rendered in this case; instead, the existing render items created will be reused.

This is the purpose for the 'renderer' property: when you call the rendering methods, a "position" is kept, and if the type is the same as that of the renderer of the RenderContext at that position, it will be reused.

Of course, the update doesn't get applied to, say, DOM, right away, as there may still be actions (such as injecting a string into DOM) that force the entire set to be re-rendered anyway; this just keeps tree state so that if it can be rendered in part, it will be.

DOMRenderContext

DOMRenderContext is the RenderContext created for DOMLayers. While all RenderContexts have layers, if a child DOMRenderContext has a different layer than the DOMRenderContext, DOMRenderContext will manage that layer's position in DOM (SC.View will cease to handle any of this).

This can actually handle some pretty hairy scenarios, but there are a few it can't handle. The child's location is only managed by its parent. This means that if its parent does not have its own layer (that is, it shares its parent's layer), the layer of the child inside is not guaranteed to actually be put in DOM in that position: there is nothing to insertBefore, and apendChild to the parent layer will append to the end, past any other RenderContexts.

We could handle this more properly (and maybe eventually we will handle all cases that don't invole string render contexts), but this code can be extremely simple for now; we may only need a few dozen lines if we keep these restrictions.

DOMRenderContext has some DOM-specific APIs that are used by the really low-level renderers that have to talk semi-directly to DOM:

  • attr(hashOrKey, [value])])
  • css(hashOrKey, [value])
  • begin(tagname)
    • note: this actually begins a new DOMRenderContext, but as this context will never be able to find its element properly (unless someone implements some mojo), this RenderContext is limited to rendering to strings, and as such, has the same restrictions as push()
  • end()
  • push(strings)
  • $()
    • only accessible during update cycle and will not work properly if you use push, begin, or end in that cycle (in short: identical to how it currently works).

DOMRenderContexts can render in two modes:

  • HTML Strings
  • Updating DOM

Sometimes, a DOMRenderContext can not render by updating DOM. This occurs if any of the following are true:

  • The DOMRenderContext contains strings (added by push, begin/end)
  • The DOMRenderContext contains child contexts that cannot render by updating and which share the same layer (in short, are not at all isolated).
  • The DOMRenderContext has not been written to layer before.
    • note: child contexts are created from the parent contexts, and cannot render before their parent contexts, and as such, covers initial render-to-string as well.

In either of these two cases, a new set of strings will have to be generated for the whole context, including any child contexts (even if they have their own layer).

While this process is not overly expensive (and, as the DOMRenderContext tree is already generated, will not cause any render() methods to be called), the injection of the strings into DOM, necessitating a full re-parsing, etc., can be expensive; as such, you generally only want to call begin(), end(), and push() to be leaf nodes in the tree.

Rendering Process

Views start the rendering process with code like the following:

updateLayer: function() {
  var context = this.get('layer').createContext(this.get('theme'));
  this.render(context);
  context.update();
}

Layers always have content, even if that content is empty. RenderContexts update it with the contents of their tree. If possible, this update will be done incrementally.

Updating

Incremental Updating is not actually a feature of the basic rendering architecture. It only applies to the DOM rendering architecture.

The tree is always maintained by render(). However, individual items in the tree can be modified outside of the tree itself--specifically, renderers, which are technically part of the view tree (as they render to render contexts).

Views must keep their renderers up-to-date either through their render() method or some other method (directly updating through observers, etc.).

Once views have updates they'd like to apply, they call the context's update() method, just like they do during a full render.

When update() is called, two things need to happen:

  • render() must be called on any changed renderers to allow them to update the Rendering Tree.
    • Renderers with canUpdateDOM set to YES will NOT have render() called.
      • value of canUpdateDOM could change depending on renerer attr changes.
    • renderers unable to update DOM might still update incrementally: if the renderers don't push any strings to the context, for example, they will end up updating incrementally.
    • Usually, only leaf renderers are ever incrementally updatable.
  • update() must be called to sync any renderer

This can be implemented by having context.update() simply call update() on any child contexts. This will cause any renderers managed by the render contexts to run render again if their canUpdateDOM is not set to YES; views handle their own render() calls, so that is not a concern.

At the end of the run loop, .sync() on the root RenderContext is called, and the tree is synced with DOM. Along with updating string contents as needed, this calls update() on any renderer that had render() skipped earlier because canUpdateDOM was YES.

  • NOTE: due to the difficulty of managing ordering of children inside parent contexts, all such changes are performed immediately if the children are already in DOM. I believe this is identical to the current SC view layer.

  • NOTE the SECOND: using jQuery or modifying DOM directly from inside render() is highly discouraged unless it is a one-off view that has no child views; in such a case, you control all updates to that segment of the Render Tree and as such (and because !firstTime will never be called if the RenderContext has not yet synced to DOM), it will work. )

SC.Theme

Themes will be changed only a small amount from current 1.5. Most of these changes are just polish.

While most themes will continue to use their "renderers" property--a proxy for their own prototype--they will no longer be forced to (or limited to) using it; the property is only accessed by the theme instance's getRenderer method, which may be overriden.

Further, the syntax for subthemes has changed:

MyTheme.subtheme("my-subtheme");

This is a shortcut for doing this:

// name automatically camelized
MyTheme.MySubtheme = MyTheme.extend({ name: 'my-subtheme' });

Note that the theme names will be added as class names to all layers, including view layers and layers generated by renderers.

Implementation Strategy

Implementing this all at once would be a bit too large of a project. Instead, it may be best to proceed by implementing this incrementally.

Here are some first few steps we could implement. Most of these should be relatively simple and quick, yet give immediate benefit and bring us towards the eventual API. We may want to do these, at least, before releasing 1.5.

  1. Add .render() method to RenderContext to make using renderers easier.
  2. Make SC.View use context.render() from its render function instead of the createRenderer/updateRenderer mess there is currently. This should also remove a lot of ugly code that determines which method of rendering should be used.
  3. Add $(), etc. to RenderContext so renderer's update() can use it instead of "this.layer()"
  4. Make all renderers use context.$(), etc. instead of this.layer(); remove attachLayer/detachLayer.

And, a bit more challenging: 5. Make SC.Layer and SC.DOMLayer. These are very simple classes that will just wrap DOM elements. The important yet challenging part is removing all use of this.get('layer') to get the DOM element in views, as it will now no longer return the DOM element. Keep in mind, eventually no SC view should ever use the DOM element directly (developers' views may, however).

These changes do not actually change the internal architecture much at all, but move most code so the architecture change would be mostly invisible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment