README.md 15.7 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374
Sky Rendering
=============

The Sky render tree is a low-level layout and painting system based on a
retained tree of objects that inherit from [`RenderObject`](object.dart). Most
developers using Sky will not need to interact directly with the rendering tree.
Instead, most developers should use [Sky widgets](../widgets/README.md), which
are built using the render tree.

Overview
--------

### Base Model

The base class for every node in the render tree is
[`RenderObject`](object.dart), which defines the base layout model. The base
layout mode is extremely general and can accomodate a large number of more
concrete layout models that can co-exist in the same tree. For example, the base
model does not commit to a fixed number of dimensions or even a cartesian
coordinate system. In this way, a single render tree can contain render objects
operating in three-dimensional space together with other render objects
operating in two-dimensional space, e.g., on the face of a cube in the three-
dimensional space. Moreover, the two-dimensional layout might be partially
computed in cartesian coordinates and partially computed in polar coordinates.
These distinct models can interact during layout, for example determining the
size of the cube by the height of a block of text on the cube's face.

Not entirely free-wheeling, the base model does impose some structure on the
render tree:

 * Subclasses of `RenderObject` must implement a `performLayout` function that
   takes as input a `constraints` object provided by its parent. `RenderObject`
   has no opinion about the structure of this object and different layout models
   use different types of constraints. However, whatever type they choose must
   implement `operator==` in such a way that `performLayout` produces the same
   output for two `constraints` objects that are `operator==`.

 * Implementations of `performLayout` are expected to call `layout` on their
   children. When calling `layout`, a `RenderObject` must use the
   `parentUsesSize` parameter to declare whether its `performLayout` function
   depends on information read from the child. If the parent doesn't declare
   that it uses the child's size, the edge from the parent to the child becomes
   a _relayout boundary_, which means the child (and its subtree) might undergo
   layout without the parent undergoing layout.

 * Subclasses of `RenderObject` must implement a `paint` function that draws a
   visual representation of the object onto a `PaintingCanvas`. If
   the `RenderObject` has children, the `RenderObject` is responsible for
   painting its children using the `paintChild` function on the
   `PaintingCanvas`.

 * Subclasses of `RenderObject` must call `adoptChild` whenever they add a
   child. Similarly, they must call `dropChild` whenever they remove a child.

 * Most subclasses of `RenderObject` will implement a `hitTest` function that
   lets clients query the render tree for objects that intersect with a given
   user input location. `RenderObject` itself does not impose a particular
   type signature on `hitTest`, but most implementations will take an argument
   of type `HitTestResult` (or, more likely, a model-specific subclass of
   `HitTestResult`) as well as an object that describes the location at which
   the user provided input (e.g., a `Point` for a two-dimensional cartesian
   model).

 * Finally, subclasses of `RenderObject` can override the default, do-nothing
   implemenations of `handleEvent` and `rotate` to respond to user input and
   screen rotation, respectively.

The base model also provides two mixins for common child models:

 * `RenderObjectWithChildMixin` is useful for subclasses of `RenderObject` that
   have a unique child.

 * `ContainerRenderObjectMixin` is useful for subclasses of `RenderObject` that
   have a child list.

Subclasses of `RenderObject` are not required to use either of these child
models and are free to invent novel child models for their specific use cases.

### Parent Data

TODO(ianh): Describe the parent data concept.

The `setupParentData()` method is automatically called for each child
when the child's parent is changed. However, if you need to
preinitialise the `parentData` member to set its values before you add
a node to its parent, you can preemptively call that future parent's
`setupParentData()` method with the future child as the argument.

TODO(ianh): Discuss putting per-child configuration information for
the parent on the child's parentData.

If you change a child's parentData dynamically, you must also call
markNeedsLayout() on the parent, otherwise the new information will
not take effect until something else triggers a layout.

### Box Model

#### Dimensions

All dimensions are expressed as logical pixel units. Font sizes are
also in logical pixel units. Logical pixel units are approximately
96dpi, but the precise value varies based on the hardware, in such a
way as to optimise for performance and rendering quality while keeping
interfaces roughly the same size across devices regardless of the
hardware pixel density.

Logical pixel units are automatically converted to device (hardware)
pixels when painting by applying an appropriate scale factor.

TODO(ianh): Define how you actually get the device pixel ratio if you
need it, and document best practices around that.

#### EdgeDims

#### BoxConstraints

### Bespoke Models


Using the provided subclasses
-----------------------------

### render_box.dart
#### RenderConstrainedBox
#### RenderShrinkWrapWidth
#### RenderOpacity
#### RenderColorFilter
#### RenderClipRect
#### RenderClipOval
#### RenderPadding
#### RenderPositionedBox
#### RenderImage
#### RenderDecoratedBox
#### RenderTransform
#### RenderSizeObserver
#### RenderCustomPaint
### RenderBlock (render_block.dart)
### RenderFlex (render_flex.dart)
### RenderParagraph (render_paragraph.dart)
### RenderStack (render_stack.dart)

Writing new subclasses
----------------------

### The RenderObject contract

If you want to define a `RenderObject` that uses a new coordinate
system, then you should inherit straight from `RenderObject`. Examples
of doing this can be found in [`RenderBox`](box.dart), which deals in
rectangles in cartesian space, and in the [sector_layout.dart
example](../../example/rendering/sector_layout.dart), which
implements a toy model based on polar coordinates. The `RenderView`
class, which is used internally to adapt from the host system to this
rendering framework, is another example.

A subclass of `RenderObject` must fulfill the following contract:

* It must fulfill the [AbstractNode contract](../base/README.md) when
  dealing with children. Using `RenderObjectWithChildMixin` or
  `ContainerRenderObjectMixin` can make this easier.

* Information about the child managed by the parent, e.g. typically
  position information and configuration for the parent's layout,
  should be stored on the `parentData` member; to this effect, a
  ParentData subclass should be defined and the `setupParentData()`
  method should be overriden to initialise the child's parent data
  appropriately.

* Layout constraints must be expressed in a Constraints subclass. This
  subclass must implement `operator==` (and `hashCode`).

* Whenever the layout needs updating, the `markNeedsLayout()` method
  should be called.

* Whenever the rendering needs updating without changing the layout,
  the `markNeedsPaint()` method should be called. (Calling
  `markNeedsLayout()` implies a call to `markNeedsPaint()`, so you
  don't need to call both.)

* The subclass must override `performLayout()` to perform layout based
  on the constraints given in the `constraints` member. Each object is
  responsible for sizing itself; positioning must be done by the
  object calling `performLayout()`. Whether positioning is done before
  or after the child's layout is a decision to be made by the class.
  TODO(ianh): Document sizedByParent, performResize(), rotate

* TODO(ianh): Document painting, hit testing, debug*

#### The ParentData contract

#### Using RenderObjectWithChildMixin

#### Using ContainerRenderObjectMixin (and ContainerParentDataMixin)

This mixin can be used for classes that have a child list, to manage
the list. It implements the list using linked list pointers in the
`parentData` structure.

TODO(ianh): Document this mixin.

Subclasses must follow the following contract, in addition to the
contracts of any other classes they subclass:

* If the constructor takes a list of children, it must call addAll()
  with that list.

TODO(ianh): Document how to walk the children.

### The RenderBox contract

A `RenderBox` subclass is required to implement the following contract:

* It must fulfill the [AbstractNode contract](../base/README.md) when
  dealing with children. Note that using `RenderObjectWithChildMixin`
  or `ContainerRenderObjectMixin` takes care of this for you, assuming
  you fulfill their contract instead.

* If it has any data to store on its children, it must define a
  BoxParentData subclass and override setupParentData() to initialise
  the child's parent data appropriately, as in the following example.
  (If the subclass has an opinion about what type its children must
  be, e.g. the way that `RenderBlock` wants its children to be
  `RenderBox` nodes, then change the `setupParentData()` signature
  accordingly, to catch misuse of the method.)

```dart
  class FooParentData extends BoxParentData { ... }

  // In RenderFoo
  void setupParentData(RenderObject child) {
    if (child.parentData is! FooParentData)
      child.parentData = new FooParentData();
  }
```

* The class must encapsulate a layout algorithm that has the following
  features:

** It uses as input a set of constraints, described by a
   BoxConstraints object, and a set of zero or more children, as
   determined by the class itself, and has as output a Size (which is
   set on the object's own `size` field), and positions for each child
   (which are set on the children's `parentData.position` field).

** The algorithm can decide the Size in one of two ways: either
   exclusively based on the given constraints (i.e. it is effectively
   sized entirely by its parent), or based on those constraints and
   the dimensions of the children.

   In the former case, the class must have a sizedByParent getter that
   returns true, and it must have a `performResize()` method that uses
   the object's `constraints` member to size itself by setting the
   `size` member. The size must be consistent, a given set of
   constraints must always result in the same size.

   In the latter case, it will inherit the default `sizedByParent`
   getter that returns false, and it will size itself in the
   `performLayout()` function described below.

   The `sizedByParent` distinction is purely a performance
   optimisation. It allows nodes that only set their size based on the
   incoming constraints to skip that logic when they need to be
   re-laid-out, and, more importantly, it allows the layout system to
   treat the node as a _layout boundary_, which reduces the amount of
   work that needs to happen when the node is marked as needing
   layout.

* The following methods must report numbers consistent with the output
  of the layout algorithm used:

** `double getMinIntrinsicWidth(BoxConstraints constraints)` must
   return the width that fits within the given constraints below which
   making the width constraint smaller would not increase the
   resulting height, or, to put it another way, the narrowest width at
   which the box can be rendered without failing to lay the children
   out within itself.

   For example, the minimum intrinsic width of a piece of text like "a
   b cd e", where the text is allowed to wrap at spaces, would be the
   width of "cd".

** `double getMaxIntrinsicWidth(BoxConstraints constraints)` must
   return the width that fits within the given constraints above which
   making the width constraint larger would not decrease the resulting
   height.

   For example, the maximum intrinsic width of a piece of text like "a
   b cd e", where the text is allowed to wrap at spaces, would be the
   width of the whole "a b cd e" string, with no wrapping.

** `double getMinIntrinsicHeight(BoxConstraints constraints)` must
   return the height that fits within the given constraints below
   which making the height constraint smaller would not increase the
   resulting width, or, to put it another way, the shortest height at
   which the box can be rendered without failing to lay the children
   out within itself.

   The minimum intrinsic height of a width-in-height-out algorithm,
   like English text layout, would be the height of the text at the
   width that would be used given the constraints. So for instance,
   given the text "hello world", if the constraints were such that it
   had to wrap at the space, then the minimum intrinsic height would
   be the height of two lines (and the appropriate line spacing). If
   the constraints were such that it all fit on one line, then it
   would be the height of one line.

** `double getMaxIntrinsicHeight(BoxConstraints constraints)` must
   return the height that fits within the given constraints above
   which making the height constraint larger would not decrease the
   resulting width. If the height depends exclusively on the width,
   and the width does not depend on the height, then
   `getMinIntrinsicHeight()` and `getMaxIntrinsicHeight()` will return the
   same number given the same constraints.

   In the case of English text, the maximum intrinsic height is the
   same as the minimum instrinsic height.

* The box must have a `performLayout()` method that encapsulates the
  layout algorithm that this class represents. It is responsible for
  telling the children to lay out, positioning the children, and, if
  sizedByParent is false, sizing the object.

  Specifically, the method must walk over the object's children, if
  any, and for each one call `child.layout()` with a BoxConstraints
  object as the first argument, and a second argument named
  `parentUsesSize` which is set to true if the child's resulting size
  will in any way influence the layout, and omitted (or set to false)
  if the child's resulting size is ignored. The children's positions
  (`child.parentData.position`) must then be set.

  (Calling `layout()` can result in the child's own `performLayout()`
  method being called recursively, if the child also needs to be laid
  out. If the child's constraints haven't changed and the child is not
  marked as needing layout, however, this will be skipped.)

  The parent must not set a child's `size` directly. If the parent
  wants to influence the child's size, it must do so via the
  constraints that it passes to the child's `layout()` method.

  If an object's `sizedByParent` is false, then its `performLayout()`
  must also size the object (by setting `size`), otherwise, the size
  must be left untouched.

* The `size` member must never be set to an infinite value.

* The box must also implement `hitTestChildren()`.
  TODO(ianh): Define this better

* The box must also implement `paint()`.
  TODO(ianh): Define this better

#### Using RenderProxyBox

### The Hit Testing contract


Performance rules of thumb
--------------------------

* Avoid using transforms where mere maths would be sufficient (e.g.
  draw your rectangle at x,y rather than translating by x,y and
  drawing it at 0,0).

* Avoid using save/restore on canvases.


Useful debugging tools
----------------------

This is a quick way to dump the entire render tree to the console every frame.
This can be quite useful in figuring out exactly what is going on when
working with the render tree.

```dart
375
import 'package:sky/src/rendering/sky_binding.dart';
376 377 378 379 380 381 382 383 384 385 386 387 388 389
import 'package:sky/base/scheduler.dart' as scheduler;

scheduler.addPersistentFrameCallback((_) {
  SkyBinding.instance.debugDumpRenderTree();
});
```


Dependencies
------------

 * [`package:sky/base`](../base)
 * [`package:sky/mojo`](../mojo)
 * [`package:sky/animation`](../mojo)