๐Ÿ–Š ink/ ยท 02_ink_dom___layout_engine.md

Chapter 2: Ink DOM & Layout Engine

๐Ÿ“„ ink/02_ink_dom___layout_engine.md

Chapter 2: Ink DOM & Layout Engine

In the previous chapter, Core Component Primitives, we learned how to compose UI using <Box> and <Text>.

But here is the mystery: The terminal is just a grid of characters. It doesn't know what a "Flexbox" is. It doesn't know what a "component" is. It doesn't even know what a "parent" or "child" is.

So, how does Ink know that justifyContent="center" means "calculate the width of the window, subtract the width of the content, divide by two, and place the cursor there"?

The answer is the Ink DOM and the Yoga Layout Engine.


The Motivation: An Invisible Skeleton

Imagine you are building a house. Before you paint the walls (render pixels), you need a blueprint and a wooden frame (DOM) to hold everything together.

Browsers have the HTML DOM (<div>, <span>). Ink creates its own DOM (Document Object Model) solely in memory.

The Goal: A Centered Modal

Let's look at this simple layout:

<Box height={10} alignItems="center" justifyContent="center">
  <Text>I am centered!</Text>
</Box>

To make this work, Ink needs to perform three invisible steps:

  1. Construct a Tree: Remember that <Text> is inside <Box>.
  2. Calculate Math: Figure out the exact X and Y coordinates for the text based on the Box's size.
  3. Store Data: Hold the style information (colors, bold) until we are ready to print.

Concept 1: The Ink DOM Node

In a browser, you have HTMLElement. In Ink, we have DOMElement.

An InkNode (or DOMElement) is a plain JavaScript object that represents one piece of your UI. It acts as a container for:

  1. Children: Which nodes are inside this one?
  2. Styles: What colors or margins does this node have?
  3. The Yoga Node: A reference to the math engine (more on this below).

What a Node looks like

If you could inspect Ink's memory while running the example above, a simplified ink-box node would look like this:

// A simplified view of an Ink Node
const boxNode = {
  nodeName: 'ink-box',
  parentNode: rootNode,
  childNodes: [textNode], // The "I am centered" node
  style: { alignItems: 'center' },
  // The layout engine instance
  yogaNode: <YogaNode Instance>
};

Concept 2: Yoga Layout Engine

Terminals don't come with CSS layout engines. Browsers do. To bridge this gap, Ink uses Yoga.

Yoga is a layout engine written in C++ (and compiled to JavaScript) that implements Flexbox. It does the heavy math.

Think of Yoga as a calculator:

  1. You tell it: "I have a parent 100px wide, and a child 10px wide. I want the child centered."
  2. You say: "Calculate!"
  3. Yoga replies: "The child should start at X coordinate 45."

How It Works: The Flow

When your React code runs, it talks to the Ink DOM, which talks to Yoga.

sequenceDiagram participant React as React Component participant DOM as Ink DOM Node participant Yoga as Yoga Engine React->>DOM: Create Node (ink-box) React->>DOM: Set Style { justifyContent: 'center' } DOM->>Yoga: Node.setJustifyContent(Center) Note over DOM, Yoga: Later, during render... DOM->>Yoga: CalculateLayout() Yoga-->>DOM: Result: { left: 45, top: 5 }
  1. Creation: React asks Ink to create a node.
  2. Configuration: Ink passes styles to the Yoga node.
  3. Calculation: Ink triggers a layout calculation.
  4. Result: The Yoga node now knows exactly where it sits in the terminal grid.

Implementation Details

Let's look under the hood at how Ink implements this "Invisible Skeleton."

1. Creating a Node (dom.ts)

When Ink initializes a component, it creates a DOMElement. Notice how it initializes the yogaNode immediately.

// dom.ts (Simplified)
import { createLayoutNode } from './layout/engine.js';

export const createNode = (nodeName) => {
  return {
    nodeName,
    childNodes: [],
    style: {},
    // Every visual element gets a Yoga node for math
    yogaNode: createLayoutNode(), 
    dirty: false
  };
};

Explanation:

2. Translating Styles to Math (styles.ts)

When you write <Box margin={1}>, Ink has to translate that React prop into a Yoga instruction.

// styles.ts (Simplified)
const applyMarginStyles = (node, style) => {
  // If the user set a margin prop...
  if ('margin' in style) {
    // ...tell Yoga to apply it to All sides.
    // LayoutEdge.All maps to Yoga.EDGE_ALL
    node.setMargin(LayoutEdge.All, style.margin ?? 0);
  }
};

Explanation:

3. The Yoga Adapter (layout/yoga.ts)

Ink wraps the raw Yoga library to make it safer and easier to use with TypeScript.

// layout/yoga.ts (Simplified)
export class YogaLayoutNode {
  constructor(yogaNode) {
    this.yoga = yogaNode;
  }

  calculateLayout(width, height) {
    // Trigger the heavy C++ calculation
    this.yoga.calculateLayout(width, height, Direction.LTR);
  }

  getComputedLeft() {
    // Retrieve the calculated X position
    return this.yoga.getComputedLeft();
  }
}

Explanation:


Putting It Together: The "Server Status" Example

Recalling our example from Chapter 1:

<Box borderStyle="single">
  <Text>Status: OK</Text>
</Box>

Here is the lifecycle in the Ink DOM:

  1. Construction:
  1. Styling:
  1. Measurement:
  1. Layout Calculation:

Now the "Skeleton" is complete. Every node knows its X, Y, Width, and Height.

Summary

In this chapter, we learned:

We have a structure. We have positions. But how does React manage updates to this structure when data changes?

Next Chapter: React Reconciler


Generated by Code IQ