# Add canvas tools

Register a tool against a mounted diagram, let it own a pointer gesture, and dispose the registration when the host is removed.

## When to use this

Use a canvas tool when the gesture belongs to your application rather than to a diagram node or link. Grafloria ships a rectangle tool, so this example uses it instead of implementing rectangle geometry. A drag on empty canvas creates a real `NodeModel`; the node is rendered at the dragged position and size.

`registerTool()` returns a disposer. Keep that function with the mounted component and call it during teardown. Registering another tool with the same id replaces the previous registration; disposing the new registration restores the previous tool.

## Mount the tool

The tool needs the live diagram model, viewport, container, and a repaint function. Build that small host from the mounted [`DiagramInstance`](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/grafloria-renderer-instance-diagraminstance#diagraminstance). The host adapter is structural: it does not create a second diagram.

### JavaScript

```html title="index.html"
<div id="diagram" style="height:100vh"></div>
```

```js title="main.js"
const diagramElement = document.querySelector('#diagram');
if (!(diagramElement instanceof HTMLElement)) {
  throw new Error('Expected #diagram');
}
diagramElement.style.height = '100vh';

import { render } from '@grafloria/element';
import {
  registerTool,
  createRectangleTool,
} from '@grafloria/renderer';

const host = diagramElement;

const instance = render({
  nodes: [{
    id: 'box1',
    position: { x: 120, y: 100 },
    size: { width: 300, height: 180 },
    label: 'Box',
    style: { shape: 'rectangle', fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2 },
  }],
  edges: [],
}, host);

const toolHost = {
  getModel: () => instance.getModel(),
  viewport: instance.viewport,
  container: host,
  render: () => instance.renderNow(),
  getEngine: () => instance.getEngine(),
};

const disposeTool = registerTool(createRectangleTool(toolHost, {
  fill: '#dbeafe',
  stroke: '#2563eb',
  strokeWidth: 2,
  label: 'Box',
}));

// Call this when the page or the diagram host is removed.
export function disposeDiagram() {
  disposeTool();
  instance.dispose();
}
```

Give `#diagram` a resolved height, for example `<div id="diagram" style="height: 100vh"></div>`. Press on empty canvas and drag corner to corner. The tool claims the gesture, then adds one rectangular node and repaints the mounted instance.

Try the working [rectangle demo](https://grafloria.com/demos/whiteboard/rectangle.html) and its [source](https://github.com/grafloria/grafloria/blob/6538c1506102712e4265e25bd8ffbb240b3e503d8/demos/whiteboard/rectangle.html).

### Angular

Register after the canvas exposes its engine, and dispose in `ngOnDestroy`.

```ts title="rectangle.component.ts"
import { AfterViewInit, Component, ElementRef, OnDestroy, viewChild } from '@angular/core';
import { DiagramCanvasComponent } from '@grafloria/angular';
import { registerTool, createRectangleTool } from '@grafloria/element';
import type { WhiteboardHost } from '@grafloria/element';

@Component({
  standalone: true,
  imports: [DiagramCanvasComponent],
  template: `
    <div #host style="display:block;height:100vh">
      <grafloria-diagram-canvas
        [(nodes)]="nodes"
        [(edges)]="edges"
        style="display:block;height:100%" />
    </div>
  `,
})
export class RectangleComponent implements AfterViewInit, OnDestroy {
  readonly canvas = viewChild.required(DiagramCanvasComponent);
  readonly host = viewChild.required<ElementRef<HTMLElement>>('host');
  nodes = [{
    id: 'box1', position: { x: 120, y: 100 }, size: { width: 300, height: 180 }, label: 'Box',
  }];
  edges = [];
  private disposeTool: (() => void) | undefined;

  ngAfterViewInit(): void {
    const canvas = this.canvas();
    const engine = canvas.activeEngine();
    const model = engine?.getDiagram();
    const viewport = canvas.viewportController();
    if (!model || !viewport) return;

    const instanceHost: WhiteboardHost = {
      getModel: () => model,
      viewport,
      container: this.host().nativeElement,
      render: () => canvas.scheduleRender(),
      getEngine: () => engine ?? null,
    };
    this.disposeTool = registerTool(createRectangleTool(instanceHost, {
      fill: '#dbeafe',
      stroke: '#2563eb',
      strokeWidth: 2,
      label: 'Box',
    }));
  }

  ngOnDestroy(): void {
    this.disposeTool?.();
  }
}
```

Angular owns the mounted element, while the tool uses the instance-backed host only for the gesture.

### Qwik

In Qwik, register the tool from the browser-side `onInit$` callback and keep its disposer in a non-serializable signal. Call that disposer from `useVisibleTask$` cleanup when the component leaves the page.

```text title="Qwik"
const dispose = noSerialize(registerTool(createRectangleTool({
  getModel: () => instance.getModel(),
  viewport: instance.viewport,
  container: host,
  render: () => instance.renderNow(),
  getEngine: () => instance.getEngine(),
}, { fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2, label: 'Box' })));

useVisibleTask$(({ cleanup }) => {
  cleanup(() => dispose?.());
});
```

### React

Store the disposer in a ref so a re-render does not lose it, and dispose it in the unmount effect.

```tsx title="RectangleDemo.tsx"
import { useEffect, useRef } from 'react';
import { GrafloriaFlow, type DiagramInstance } from '@grafloria/react';
import { registerTool, createRectangleTool } from '@grafloria/element';
import type { WhiteboardHost } from '@grafloria/element';

export default function RectangleDemo() {
  const hostRef = useRef<HTMLDivElement | null>(null);
  const disposeRef = useRef<(() => void) | undefined>(undefined);

  useEffect(() => () => disposeRef.current?.(), []);

  const onInit = (instance: DiagramInstance) => {
    const host = hostRef.current;
    if (!host) return;
    const instanceHost: WhiteboardHost = {
      getModel: () => instance.getModel(),
      viewport: instance.viewport,
      container: host,
      render: () => instance.renderNow(),
      getEngine: () => instance.getEngine(),
    };
    disposeRef.current = registerTool(createRectangleTool(instanceHost, {
      fill: '#dbeafe',
      stroke: '#2563eb',
      strokeWidth: 2,
      label: 'Box',
    }));
  };

  return (
    <div ref={hostRef} style={{ display: 'block', height: '100vh' }}>
      <GrafloriaFlow defaultNodes={[{ id: 'box1', position: { x: 120, y: 100 }, size: { width: 300, height: 180 }, label: 'Box', style: { shape: 'rectangle', fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2 } }]} defaultEdges={[]} onInit={onInit} style={{ display: 'block', height: '100%' }} />
    </div>
  );
}
```

### Vue

Capture the instance from `@init`, then dispose the registration from `onBeforeUnmount`.

```vue title="RectangleDemo.vue"
<script setup lang="ts">
import { onBeforeUnmount, ref } from 'vue';
import { GrafloriaFlow } from '@grafloria/vue';
import type { DiagramInstance } from '@grafloria/vue';
import { registerTool, createRectangleTool } from '@grafloria/element';
import type { WhiteboardHost } from '@grafloria/element';

const host = ref<HTMLElement | null>(null);
let disposeTool: (() => void) | undefined;

function onInit(instance: DiagramInstance): void {
  if (!host.value) return;
  const instanceHost: WhiteboardHost = {
    getModel: () => instance.getModel(),
    viewport: instance.viewport,
    container: host.value,
    render: () => instance.renderNow(),
    getEngine: () => instance.getEngine(),
  };
  disposeTool = registerTool(createRectangleTool(instanceHost, {
    fill: '#dbeafe',
    stroke: '#2563eb',
    strokeWidth: 2,
    label: 'Box',
  }));
}

onBeforeUnmount(() => disposeTool?.());
</script>

<template>
  <div ref="host" style="height:100vh">
    <GrafloriaFlow :default-nodes="[{ id: 'box1', position: { x: 120, y: 100 }, size: { width: 300, height: 180 }, label: 'Box', style: { shape: 'rectangle', fill: '#dbeafe', stroke: '#2563eb', strokeWidth: 2 } }]" style="height:100%" @init="onInit" />
  </div>
</template>
```

## Tool options

These are the options used by the shipped rectangle implementation in the example.

| Option | Type | Default | What it does |
|---|---|---|---|
| `fill` | `string` | Not specified | Sets the new rectangle's fill. |
| `stroke` | `string` | Not specified | Sets the new rectangle's outline. |
| `strokeWidth` | `number` | Not specified | Sets the outline width. |
| `label` | `string` | Not specified | Sets the label on each new rectangle. |

## Arbitration and cleanup

`hitTest` decides which registered tool claims a pointerdown. When several tools claim the same gesture, the highest explicit `priority` wins; ties use registration order, which is not a contract to rely on. A point-specific tool should use a higher priority than a broad mode tool when both can claim the same pointerdown.

Dispose every registration owned by a routed page or component. The disposer also calls the tool's cleanup when the registration is removed, and restores a previous tool with the same id. Do not leave a global registration active after its diagram disappears.

## Related

- [How Grafloria works](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/how-grafloria-works) explains the instance, model, and engine layers.
- [Events and interaction](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/events-and-interaction) covers the interaction model beneath canvas gestures.
- [Customize nodes](https://bench-grafloria-56.atloria.app/p/bench-grafloria-56-62cg8vR5rF/developer/customize-nodes) covers rendering node content rather than claiming a canvas gesture.
