Skip to content

Instantly share code, notes, and snippets.

@manu-unter
Last active June 1, 2020 17:40
Show Gist options
  • Save manu-unter/27ef0bd67b3f5cb9e4f77abccf9d4aa5 to your computer and use it in GitHub Desktop.
Save manu-unter/27ef0bd67b3f5cb9e4f77abccf9d4aa5 to your computer and use it in GitHub Desktop.
Thoughts and ideas around possible component-based APIs for layout and drawing in revery (WIP)

Component-Based Layout and Drawing

This is a work in progress and it should serve as a basis for continuing brainstorming and for bouncing ideas with others. The code should be considered pseudocode and it doesn't necessarily compile in ReasonML or any language for that matter.

Assumptions

  • Layout deals with screen space in Cartesian coordinates: x and y, width and height
  • There is a matrix stack which transforms all coordinates prior to drawing. This stack is hidden to the components but it will be used by the renderer to implement layout decisions along the tree.
  • Drawing is done using the painter's algorithm and the target is an immediate medium. The element tree will be drawn following a depth-first traversal strategy, meaning every element will be able to paint over its parents and any siblings that came before itself, along with their children.

Goals

  • The solution should optimize for idiomatic use of a component-based system with as few additional concepts and primitives as possible
  • Components should form a clear abstraction which solves a single UI problem. They should work independently from where they are placed
  • All information which is needed for drawing and layout should be provided through props or - if the use-case arises - through some kind of context API. No other inheritance, cascading, or implicit parameterization should exist
  • In line with the functional nature of ReasonML, instead of inheritance, the system should promote composition of components to allow code reuse and to ensure consistency of the rendered output
  • The system should be designed for easy and flexible composition of small, single-purpose components. It should be the norm that you can compose everything with anything. Restrictions in terms of which children are allowed inside a given component should be the absolute exception since they limit the possibilities for code reuse and solving of problems with composition. The most common cases should be that a component can accept any child or none at all, respectively.
  • Components should have full control over their visual representation. They can define their own logic for drawing themselves into a canvas using a canvas API
  • The resulting system should facilitate the construction of highly dynamic UIs for applications. Layout and design of static documents is not a priority.
  • There are lots of learnings from the web and also other existing libraries that we should use in the design of this system. We should however re-evaluate all decisions and not prematurely adopt solutions without considering the trade-offs it introduces.
  • Existing drawing APIs should be used in a performant and efficient way to allow the creation of highly responsive and resource-efficient UIs

Questions to Answer

  • How do we reconcile the matrix stack with a component API for layout? Do we just specify provided width and height plus a transform matrix as output parameters?
  • How much restriction do we need while drawing? Should components be confined to some kind of area to ensure they don't draw over other pieces of UI? Are dimensions simply numbers that a component should use for drawing or do we install checks which keep it from drawing anything outside of its allocated boundaries? Is that even possible? Would it open up possibilities to partially redraw and then recompose the output?
  • Would it make sense and would it be possible to create compositing layers through some kind of subtree separation API for optimization purposes? A given subtree could be treated differently from the rest of the UI by first rendering the parent tree and then copying it into a new frame buffer. The second frame buffer would then be used to draw the subtree into it. This would allow us to re-use the rendering result of the parent tree and optimize towards fast rerendering of the subtree. How could this work with multiple such subtrees?
  • Do we need optimizations to avoid recalculation of layout like browser engines do? Do we need to differentiate between relayout and redraw? Do we need a reflow?
  • Do we need the possibility to manually override the stacking and drawing order of subtrees? This would allow z-index style setups, which is often frowned upon for its unpredictability. It does provide a lot of flexibility though and facilitates colocation of related pieces of UI. Is this a goal worth pursuing over predictability? Is there a way to have both? Portals?
  • Do we need the possibility to ignore the matrix stack for a given subtree? This would allow position: fixed style layout, but it also makes rendering output less predictable. Maybe it makes sense to stay strict here so that everything that should have access to the whole screen estate always needs to appear at the top of the component tree? This would affect pop-overs, modals, overlays etc. This has similar trade-offs and possible also solutions with respect to colocation and predictability like the above.
  • Do we need the possibility to use static, global units in layout which aren't affected by the matrix stack? Things like px, pt, vw, vh? We could probably calculate them by applying the inverse matrix stack on the units in coordinate space.
  • What concepts do we need to create a useful dynamic layout system for anything that makes complex layout decisions based on the information that should be presented as well as the available space? preferredWidth/Height? minWidth/Height, maxWidthHeight? This is very important for text layout, but also responsive design. How does this affect our requirements towards global units?

Draft Component API

type calculateChildBounds = (props: Props, childIndex: int, childCount: int, getChildLayoutPreferences: unit => layoutPreferences) => (ownWidth: float, ownHeight: float) => (childTransform: matrix44, childWidth: float, childHeight: float);
type getLayoutPreferences = (props: Props, totalWidth: float, totalHeight: float, 

Use-Cases and Features to Consider

Top-Down Layout

The parent calculates the transform of its children based only its own props, and the count of children. The parent doesn't consider any child preferences when doing so. This actually gets us susprisingly far already.

Solved Use-Cases and Drafts

  • Overlaying children/Single child using the same bounds: Returning the identity transform: (props, childIndex, childCount) => identity;
  • Static layout (transformed with whatever the parents did on the way) : (props, childIndex, childCount) => translate(props.x, props.y);
  • Transformations: (props, childIndex, childCount) => scale(props.scale);
  • Stacks, basic tables, basic grids: (props, childIndex, childCount) => scaleAndTransform(1., 1. /. childCount, 0., childIndex /. childCount);

Bottom-Up Layout

Every component can implement a function which the parent will call to determine the preferred size of an element, possibly with other additional information along with it. The exact shape of that information might need a bit more thought. A single number per dimension might be too little to be practical - maybe we need three like preferredMinWidth, preferredWidth, preferredMaxWidth or similar - possibly also additional information like a grow or shrink factor/parameter, similar to flex-grow and flex-shrink on the web?

Solved Use-Cases and Drafts

  • Laying out dynamic text runs: `(availableWidth, availableHeight) => (availableWidth, get
  • display:flex style layout systems ...more to come
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment