Composite

The composite is an element that it is composed of several elements and behaves like a single one. It is used like a view, but its inner workings are completely different. There are different types of composites and each one of them defines what are the elements it is composed of.

To create a composite type you first need to call the canvasUI.composite.newType(name) function passing as argument the name of the type, like so:

const sudokuType = canvasUI.composite.newType("sudoku")

This will return an object that you will use to specify all the data about the type.

Properties

After creating the composite type, you will be able to define what are the properties that it will have using the .set(property, value) method, like so:

sudokuType.set("size", 450)
sudokuType.set("lines", { color: "#777", size: 1 })

Lifecycle

The composite element goes through different lifecycle states, and it performs different tasks in each one of them:

  • Create: It creates an element. In case that the element is a layout, it may create the elements the layout contains (most likely).

  • Start: It updates the element and the elements it contains (in case that the element is a layout) and it calls the .start() method on the element.

  • Measure: It calls the .measure(maxSize) method on the element with the maximum size that was passed in its .measure(maxSize) method. Then, the size of the element is assigned to its size property.

  • Locate: It calls the .locate(coords) method on the element with the coords that were passed in its .locate(coords) method. Then, the coords of the element are assigned to its coords property.

  • Draw: It calls the .draw(ctx) method on the element with the canvas context that was passed in its .draw(ctx) method.

To perform these tasks, the composite will call its lifecycle functions. These functions are implemented differently by every composite type and to implement them you have to use the .lifecycle.set(name, func) method, like so:

compositeType.lifecycle.set("onCreate", function (composite) {
  // Code
})

Notice how the first parameter of the function is the composite itself. That happens with every lifecycle function.

The lifecycle functions of the composite type that we can use are:

  • onCreate(composite): It is called when the composite enters the create state.

  • getElement(composite): It is called in the create state. This lifecycle function needs to be implemented and in it the composite has to create and return the element (if the element is a layout the elements it contains may also be created).

  • onStart(composite): It is called when the composite enters the start state.

  • updateElement(composite, element): It is called in the start state. This lifecycle function needs to be implemented and in it the composite has to update the element (if the element is a layout the elements it contains may also be updated). The second parameter is the element that has to be updated.

  • onMeasure(composite, maxSize): It is called when the composite enters the measure state. The second parameter is the maximum size that was passed in its .measure(maxSize) method.

  • onLocate(composite, coords): It is called when the composite enters the locate state. The second parameter is the coords that were passed in its .locate(coords) method.

  • onDraw(composite): It is called when the composite enters the draw state.

  • onEnd(composite): It is called when the composite enters the end state.

Example

To give you a solid understanding about how to create a composite type, I will show you how to create a type that consists of a grid of numbers (2x2). This grid will be composed of a grid layout with a text view in each cell.

We first need to create the composite type and give it some properties:

const gridNumbersType = canvasUI.composite.newType("gridNumbers")

gridNumbersType.set("size", 200)
gridNumbersType.set("numbers", [
  [0, 0],
  [0, 0],
])
gridNumbersType.set("gap", 1)

The size property defines the size of the grid. The numbers property defines the numbers of the grid. The gap property defines the gap between the cells.

Now, we are ready to start implementing some lifecycle functions.

We will start by implementing the getElement lifecycle function of the create state, like so:

gridNumbersType.lifecycle.set("getElement", function (composite) {
  const grid = canvasUI.layout.new("grid", "grid")
  grid.set("dimensions", {
    columns: [{ count: 2, unit: "fr", value: 1 }],
    rows: [{ count: 2, unit: "fr", value: 1 }],
  })
  grid.set("alignItems", { horizontal: "middle", vertical: "middle" })
  grid.get("gap").color = "#000"
  for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
      const text = canvasUI.view.new(`text-${i},${j}`, "text")
      text.get("font").size = 16
      text.get("font").color = "#000"
      grid.insert(text)
      text.layoutParams.set("position", { column: i, row: j })
    }
  }
  return grid
})

In this function, we have created a grid layout that has two rows and two columns and contains a text view in each cell.

After doing that, we can implement the updateElement lifecycle function of the start state, like so:

gridNumbersType.lifecycle.set("updateElement", function (composite, grid) {
  const size = composite.get("size")
  const numbers = composite.get("numbers")
  const gap = composite.get("gap")

  grid.set("size", {
    width: { unit: "px", value: size },
    height: { unit: "px", value: size },
  })

  grid.get("gap").size = {
    horizontal: gap,
    vertical: gap,
  }

  for (let i = 0; i < 2; i++) {
    for (let j = 0; j < 2; j++) {
      const text = grid.find(`text-${i},${j}`)
      const number = numbers[j][i]
      text.set("text", `${number}`)
    }
  }
})

The gridNumbers type has been created, and now we are ready to use it:

const gridNumbers = canvasUI.composite.new("gridNumbers-1", "gridNumbers")

gridNumbers.set("numbers", [
  [5, 7],
  [2, 0],
])
gridNumbers.set("gap", 5)