---
title: "Tools, cursors & touch — Vue"
description: "The active tool decides what the pointer does: select, pan, draw. Switch tools, add your own, and choose cursors, touch behavior and haptics."
framework: "Vue"
source: "https://www.embedpdf.com/docs/headless/vue/ui/tools"
---

# Tools, cursors & touch

At any moment one **tool** is active, and it decides what a click or a drag on the page does: select
text, scroll, draw a rectangle, place a stamp. The interaction plugin keeps track of it, and every
feature that uses the pointer works through it. Register `interactionPlugin()`:

**`interaction-plugin.vue`**

```vue
<script setup lang="ts">
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { renderPlugin } from '@embedpdf/vue/render';
import { Viewer } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { engine } from './pdf';

const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];
</script>

<template>
  <Viewer :engine :plugins>
    <!-- your toolbar and pages -->
  </Viewer>
</template>
```

Then switch tools from your toolbar:

**`tool-switch.vue`**

```vue
<script setup lang="ts">
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';

const interaction = useInteraction();
const { activeToolId } = useInteractionState();
</script>

<template>
  <button :aria-pressed="activeToolId === 'pointer'" @click="interaction.activateTool('pointer')">
    Select
  </button>
  <button :aria-pressed="activeToolId === 'pan'" @click="interaction.activateTool('pan')">
    Hand
  </button>
</template>
```

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { localEngine } from '@embedpdf/engine';
import ToolSwitch from './ToolSwitch.vue';

import '../switch.css';

const engine = localEngine();
// The document opens with the hand tool.
const plugins = [
  stagePlugin(),
  renderPlugin(),
  interactionPlugin({ defaultTool: 'pan' }),
  selectionPlugin(),
];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <ToolSwitch />
      <Stage class="stage">
        <template #page>
          <RenderLayer />
          <SelectionLayer />
        </template>
      </Stage>
    </DocumentGate>
  </Viewer>
</template>
```

**`switch.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The tool switch, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 14px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

.segment {
  min-width: 72px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 8px 12px;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.segment:hover {
  color: var(--ink);
  background: var(--tint);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--accent-tint);
}

.segment:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.readout {
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ToolSwitch.vue`**

```vue
<script setup lang="ts">
import { useInteraction, useInteractionState } from '@embedpdf/vue/interaction';

const LABELS: Record<string, string> = { pointer: 'Select', pan: 'Hand' };
const HINTS: Record<string, string> = {
  pointer: 'A drag selects text',
  pan: 'A drag scrolls the pages',
};

// A button for every tool you can switch to, the active one pressed.
const interaction = useInteraction();
const { activeToolId, tools } = useInteractionState();
</script>

<template>
  <div class="toolbar">
    <div class="segmented" role="group" aria-label="Tool">
      <button
        v-for="tool in tools"
        :key="tool.id"
        type="button"
        class="segment"
        :aria-pressed="tool.id === activeToolId"
        @click="interaction.activateTool(tool.id)"
      >
        {{ LABELS[tool.id] ?? tool.id }}
      </button>
    </div>
    <output class="readout">{{ activeToolId ? HINTS[activeToolId] : '' }}</output>
  </div>
</template>
```

## The built-in tools

| Tool      | A drag on the page                             | Also                                     |
| :-------- | :--------------------------------------------- | :--------------------------------------- |
| `pointer` | Selects text, or selects and moves annotations | Fills forms, follows links. The default. |
| `pan`     | Scrolls, like Acrobat's hand tool              | Fills forms, follows links               |

Plugins add their own: the annotation plugin a tool per kind (`square`, `highlight`, `note`…), the form
plugin a tool per field, redaction `redact`, measurements `distance`. They're all switched the same
way, with `activateTool(id)`, and `tools` in [State](#state) lists them.

## Hold a tool for a moment

`pushTool()` switches to a tool and remembers the one before; `popTool()` goes back. That's how
"hold Space to pan" works:

**`hold-space.vue`**

```vue
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue';
import { useInteraction } from '@embedpdf/vue/interaction';

const interaction = useInteraction();

function down(event: KeyboardEvent) {
  if (event.code === 'Space' && !event.repeat) interaction.pushTool('pan');
}
function up(event: KeyboardEvent) {
  if (event.code === 'Space') interaction.popTool();
}

onMounted(() => {
  window.addEventListener('keydown', down);
  window.addEventListener('keyup', up);
});
onUnmounted(() => {
  window.removeEventListener('keydown', down);
  window.removeEventListener('keyup', up);
});
</script>

<template>
  <!-- your toolbar and pages -->
</template>
```

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { Stage, stagePlugin } from '@embedpdf/vue/stage';
import { RenderLayer, renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { SelectionLayer, selectionPlugin } from '@embedpdf/vue/selection';
import { localEngine } from '@embedpdf/engine';
import ActiveTool from './ActiveTool.vue';
import HoldSpaceToPan from './HoldSpaceToPan.vue';

import '../hold-space.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin(), selectionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <ActiveTool />
      <HoldSpaceToPan>
        <Stage class="stage">
          <template #page>
            <RenderLayer />
            <SelectionLayer />
          </template>
        </Stage>
      </HoldSpaceToPan>
    </DocumentGate>
  </Viewer>
</template>
```

**`hold-space.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The active tool, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 14px;
}

.badge {
  min-width: 56px;
  text-align: center;
  font-size: 13px;
  font-weight: 600;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: 6px 12px;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    color 0.15s ease;
}

.badge[data-active='true'] {
  color: var(--accent);
  background: var(--accent-tint);
  border-color: transparent;
}

.readout {
  font-size: 13px;
  color: var(--muted);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`ActiveTool.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import { useInteractionState } from '@embedpdf/vue/interaction';

const { activeToolId } = useInteractionState();
const panning = computed(() => activeToolId.value === 'pan');
</script>

<template>
  <div class="toolbar">
    <span class="badge" :data-active="panning">{{ panning ? 'Hand' : 'Select' }}</span>
    <span class="readout">
      {{ panning ? 'Drag to scroll, then let go of Space' : 'Hold Space over the pages to scroll' }}
    </span>
  </div>
</template>
```

**`HoldSpaceToPan.vue`**

```vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { useInteraction } from '@embedpdf/vue/interaction';

// While Space is held over the pages, the hand tool is active; on release, the tool from before.
const interaction = useInteraction();
// Only over the pages, so Space still scrolls the rest of your page.
const viewer = ref<HTMLDivElement | null>(null);
let held = false;

function down(event: KeyboardEvent) {
  if (event.code !== 'Space' || !viewer.value?.matches(':hover')) return;
  event.preventDefault();
  if (event.repeat || held) return;
  held = true;
  interaction.pushTool('pan');
}

function up(event: KeyboardEvent) {
  if (event.code !== 'Space' || !held) return;
  held = false;
  interaction.popTool();
}

onMounted(() => {
  window.addEventListener('keydown', down);
  window.addEventListener('keyup', up);
});
onUnmounted(() => {
  window.removeEventListener('keydown', down);
  window.removeEventListener('keyup', up);
});
</script>

<template>
  <div ref="viewer" class="viewer">
    <slot />
  </div>
</template>
```

`activateDefaultTool()` goes back to the default tool, `pointer` unless you set another.

## Your own tool

A tool of your own gets the pointer on the page, in page coordinates:

```ts
const remove = interaction.registerTool({
  id: 'pin',
  cursor: 'copy',
  onPointerDown: ({ page, point }) => {
    addPin(page, point);
    return true; // this tool handled the click
  },
});

interaction.activateTool('pin');
```

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { localEngine } from '@embedpdf/engine';
import PinBoard from './PinBoard.vue';

import '../pin.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <PinBoard />
    </DocumentGate>
  </Viewer>
</template>
```

**`pin.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --accent-tint: #e7f0fe;
  --pin: #e5484d;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The tools, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 12px;
  margin-bottom: 14px;
}

.segmented {
  display: inline-flex;
  gap: 2px;
  padding: 3px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 11px;
  box-shadow: 0 1px 2px var(--shadow);
}

.segment,
.button {
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1;
  cursor: pointer;
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease,
    color 0.15s ease;
}

.segment {
  min-width: 64px;
  color: var(--muted);
  background: transparent;
  border: 0;
  border-radius: 8px;
  padding: 8px 12px;
}

.segment:hover {
  color: var(--ink);
  background: var(--tint);
}

.segment[aria-pressed='true'] {
  color: var(--accent);
  background: var(--accent-tint);
}

.button {
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 10px 13px;
  box-shadow: 0 1px 2px var(--shadow);
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
}

.segment:focus-visible,
.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.readout {
  font-size: 13px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}

/* A pin: its tip on the point that was clicked */
.pin {
  position: absolute;
  box-sizing: border-box;
  min-width: 24px;
  height: 24px;
  padding: 0 5px;
  transform: translate(-50%, calc(-100% - 7px));
  display: grid;
  place-items: center;
  font-size: 11px;
  font-weight: 700;
  color: var(--surface);
  background: var(--pin);
  border: 2px solid var(--surface);
  border-radius: 999px;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.3);
  pointer-events: none;
}

.pin::after {
  content: '';
  position: absolute;
  left: 50%;
  top: 100%;
  border: 6px solid transparent;
  border-top-color: var(--pin);
  transform: translate(-50%, 1px);
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`PinBoard.vue`**

```vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { Stage } from '@embedpdf/vue/stage';
import { RenderLayer } from '@embedpdf/vue/render';
import { svgCursor, useInteraction, useInteractionState } from '@embedpdf/vue/interaction';
import PinLayer from './PinLayer.vue';
import type { Pin } from './pins';

// The cursor is the pin itself; its tip is the point that clicks.
const PIN_CURSOR = svgCursor({
  svg: `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <path d="M12 23s7.5-7.4 7.5-12.5a7.5 7.5 0 0 0-15 0C4.5 15.6 12 23 12 23z" fill="#e5484d" stroke="#fff" stroke-width="1.5"/>
    <circle cx="12" cy="10.5" r="2.8" fill="#fff"/>
  </svg>`,
  hotspot: { x: 12, y: 23 },
  fallback: 'copy',
});

const interaction = useInteraction();
const { activeToolId } = useInteractionState();
const pins = ref<readonly Pin[]>([]);

// A tool of your own: a click on a page drops a pin there, in page coordinates.
let remove: (() => void) | undefined;
onMounted(() => {
  remove = interaction.registerTool({
    id: 'pin',
    cursor: PIN_CURSOR,
    onPointerDown: ({ page, point }) => {
      pins.value = [...pins.value, { id: pins.value.length + 1, page, point }];
      return true; // this tool handled the click
    },
  });
  interaction.activateTool('pin');
});
onUnmounted(() => remove?.());
</script>

<template>
  <div class="toolbar">
    <div class="segmented" role="group" aria-label="Tool">
      <button
        type="button"
        class="segment"
        :aria-pressed="activeToolId === 'pin'"
        @click="interaction.activateTool('pin')"
      >
        Pin
      </button>
      <button
        type="button"
        class="segment"
        :aria-pressed="activeToolId === 'pan'"
        @click="interaction.activateTool('pan')"
      >
        Hand
      </button>
    </div>
    <button type="button" class="button" :disabled="pins.length === 0" @click="pins = []">
      Clear
    </button>
    <output class="readout">
      {{ pins.length === 0 ? 'Click a page to drop a pin' : `${pins.length} pins` }}
    </output>
  </div>
  <Stage class="stage">
    <template #page="{ page }">
      <RenderLayer />
      <PinLayer :page="page" :pins="pins" />
    </template>
  </Stage>
</template>
```

**`PinLayer.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import { pageRefsEqual } from '@embedpdf/vue/runtime';
import type { PageContextValue } from '@embedpdf/vue/runtime';
import type { Pin } from './pins';

// The pins on one page, placed in its pixels at the last moment.
const props = defineProps<{ page: PageContextValue; pins: readonly Pin[] }>();

const placed = computed(() =>
  props.pins
    .filter((pin) => pageRefsEqual(pin.page, props.page.ref))
    .map((pin) => ({ id: pin.id, at: props.page.transform.toPixels(pin.point) })),
);
</script>

<template>
  <span
    v-for="pin in placed"
    :key="pin.id"
    class="pin"
    :style="{ left: `${pin.at.x}px`, top: `${pin.at.y}px` }"
  >
    {{ pin.id }}
  </span>
</template>
```

**`pins.ts`**

```typescript
import type { PageRef } from '@embedpdf/vue/runtime';

// A pin, where it was dropped: its page, and a point in page coordinates.
export interface Pin {
  readonly id: number;
  readonly page: PageRef;
  readonly point: { readonly x: number; readonly y: number };
}
```

| Option                         | What it is                                                                                |
| :----------------------------- | :---------------------------------------------------------------------------------------- |
| `id`                           | The tool's name, for `activateTool(id)`                                                   |
| `cursor`                       | The cursor over the page, any CSS cursor                                                  |
| `onPointerDown`                | A press on a page: `page`, `point`, `modifiers`. Return `true` to take the gesture        |
| `onPointerMove`, `onPointerUp` | The rest of a gesture you took                                                            |
| `onHover`                      | The pointer moving over a page, with no button pressed                                    |
| `touch`                        | `'draw'`: one finger drives the tool; `'tap'` (default): a tap is a click, a drag scrolls |

`registerTool()` returns a function that removes the tool again. A tool you drag with takes the press and
follows the gesture:

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { localEngine } from '@embedpdf/engine';
import Ruler from './Ruler.vue';

import '../ruler.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <Ruler />
    </DocumentGate>
  </Viewer>
</template>
```

**`ruler.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --ruler: #e5484d;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The measurement, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 16px;
  min-height: 20px;
  margin-bottom: 14px;
}

.readout {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}

.readout.muted {
  font-size: 13px;
  font-weight: 450;
  color: var(--muted);
}

/* The line on the page, and its length at its middle */
.ruler-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
  pointer-events: none;
}

.ruler-line {
  stroke: var(--ruler);
  stroke-width: 2;
  stroke-dasharray: 6 4;
}

.ruler-end {
  fill: var(--surface);
  stroke: var(--ruler);
  stroke-width: 2;
}

.ruler-label {
  position: absolute;
  transform: translate(-50%, calc(-100% - 8px));
  font-size: 12px;
  font-weight: 600;
  white-space: nowrap;
  color: var(--surface);
  background: var(--ruler);
  border-radius: 999px;
  padding: 4px 9px;
  box-shadow: 0 2px 6px rgb(7 32 76 / 0.25);
  pointer-events: none;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`measurement.ts`**

```typescript
import type { PageRef } from '@embedpdf/vue/runtime';

export interface Point {
  readonly x: number;
  readonly y: number;
}

// A line on one page, in page coordinates.
export interface Measurement {
  readonly page: PageRef;
  readonly from: Point;
  readonly to: Point;
}

// Page coordinates are points, 72 to the inch.
export const lengthOf = ({ from, to }: Measurement) => {
  const inches = Math.hypot(to.x - from.x, to.y - from.y) / 72;
  return `${inches.toFixed(2)} in · ${(inches * 2.54).toFixed(1)} cm`;
};
```

**`Ruler.vue`**

```vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { Stage } from '@embedpdf/vue/stage';
import { RenderLayer } from '@embedpdf/vue/render';
import { useInteraction } from '@embedpdf/vue/interaction';
import RulerLayer from './RulerLayer.vue';
import { lengthOf } from './measurement';
import type { Measurement, Point } from './measurement';

const interaction = useInteraction();
const measurement = ref<Measurement | null>(null);
const pointer = ref<Point | null>(null);

// A tool you drag with: the press starts a line, the moves stretch it, the release ends it.
let remove: (() => void) | undefined;
onMounted(() => {
  remove = interaction.registerTool({
    id: 'ruler',
    cursor: 'crosshair',
    touch: 'draw', // one finger measures, two fingers scroll and zoom
    onPointerDown: ({ page, point }) => {
      measurement.value = { page, from: point, to: point };
      return true;
    },
    onPointerMove: ({ point }) => {
      if (measurement.value) measurement.value = { ...measurement.value, to: point };
    },
    onPointerUp: ({ point }) => {
      if (measurement.value) measurement.value = { ...measurement.value, to: point };
    },
    onHover: ({ point }) => {
      pointer.value = point;
    },
  });
  interaction.activateTool('ruler');
});
onUnmounted(() => remove?.());
</script>

<template>
  <div class="toolbar">
    <output class="readout">
      {{ measurement ? lengthOf(measurement) : 'Drag on a page to measure' }}
    </output>
    <output class="readout muted">
      {{ pointer ? `x ${Math.round(pointer.x)} · y ${Math.round(pointer.y)} pt` : '' }}
    </output>
  </div>
  <Stage class="stage">
    <template #page="{ page }">
      <RenderLayer />
      <RulerLayer :page="page" :measurement="measurement" />
    </template>
  </Stage>
</template>
```

**`RulerLayer.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import { pageRefsEqual } from '@embedpdf/vue/runtime';
import type { PageContextValue } from '@embedpdf/vue/runtime';
import { lengthOf } from './measurement';
import type { Measurement } from './measurement';

// The measured line on its page, placed in the page's pixels.
const props = defineProps<{ page: PageContextValue; measurement: Measurement | null }>();

const line = computed(() => {
  const { measurement, page } = props;
  if (!measurement || !pageRefsEqual(measurement.page, page.ref)) return null;
  return {
    from: page.transform.toPixels(measurement.from),
    to: page.transform.toPixels(measurement.to),
    label: lengthOf(measurement),
  };
});
</script>

<template>
  <template v-if="line">
    <svg class="ruler-layer">
      <line
        class="ruler-line"
        :x1="line.from.x"
        :y1="line.from.y"
        :x2="line.to.x"
        :y2="line.to.y"
      />
      <circle class="ruler-end" :cx="line.from.x" :cy="line.from.y" :r="4" />
      <circle class="ruler-end" :cx="line.to.x" :cy="line.to.y" :r="4" />
    </svg>
    <span
      class="ruler-label"
      :style="{
        left: `${(line.from.x + line.to.x) / 2}px`,
        top: `${(line.from.y + line.to.y) / 2}px`,
      }"
    >
      {{ line.label }}
    </span>
  </template>
</template>
```

## Cursors

Each tool has a cursor. Over things the tool can act on, the viewer switches to the right one by itself:
a move or resize cursor over an annotation, a text cursor over text, a hand over a link. You choose the
tool's own cursor, in three steps:

- **A CSS cursor**: `cursor: 'crosshair'`.
- **An image**: `cursor: svgCursor({ svg: penIcon, hotspot: { x: 2, y: 22 } })`. The SVG needs a
  `width` and a `height`, and `hotspot` is the point that clicks.
- **An image that changes**, such as a pen in the current color: `useToolCursor()` from a component, which follows the values it reads.

**`pen-cursor.vue`**

```vue
<script setup lang="ts">
import { useToolCursor } from '@embedpdf/vue/interaction';
import { penIcon } from './icons';

const props = defineProps<{ color: string }>();

useToolCursor(() => ({
  toolId: 'ink',
  cursors: { crosshair: { svg: penIcon(props.color), hotspot: { x: 2, y: 22 } } },
}));
</script>

<template>
  <slot />
</template>
```

**`App.vue`**

```vue
<script setup lang="ts">
import { DocumentGate, Viewer } from '@embedpdf/vue/runtime';
import type { OpenInput } from '@embedpdf/vue/runtime';
import { stagePlugin } from '@embedpdf/vue/stage';
import { renderPlugin } from '@embedpdf/vue/render';
import { interactionPlugin } from '@embedpdf/vue/interaction';
import { localEngine } from '@embedpdf/engine';
import Dots from './Dots.vue';

import '../cursor.css';

const engine = localEngine();
const plugins = [stagePlugin(), renderPlugin(), interactionPlugin()];

const ebook = async (): Promise<OpenInput> => {
  const response = await fetch('https://snippet.embedpdf.com/ebook.pdf');
  return { kind: 'bytes', id: 'ebook', bytes: new Uint8Array(await response.arrayBuffer()) };
};
</script>

<template>
  <Viewer :engine="engine" :plugins="plugins" :initial-documents="[{ source: ebook }]">
    <DocumentGate>
      <template #fallback><p class="loading">Loading…</p></template>
      <Dots />
    </DocumentGate>
  </Viewer>
</template>
```

**`cursor.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --surface: #ffffff;
  --canvas: #f7f9fc;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The colors and the cursor switch, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 18px;
  margin-bottom: 14px;
}

.swatches {
  display: inline-flex;
  gap: 8px;
}

.swatch {
  width: 26px;
  height: 26px;
  padding: 0;
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 0 0 1px var(--line);
  cursor: pointer;
  transition: box-shadow 0.15s ease;
}

.swatch[aria-checked='true'] {
  box-shadow: 0 0 0 2px var(--ink);
}

.swatch:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 3px;
}

.check {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 13px;
  color: var(--ink);
  cursor: pointer;
}

/* A dot where the pen clicked */
.dot {
  position: absolute;
  width: 12px;
  height: 12px;
  transform: translate(-50%, -50%);
  border: 2px solid var(--surface);
  border-radius: 50%;
  box-shadow: 0 1px 4px rgb(7 32 76 / 0.3);
  pointer-events: none;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

**`DotLayer.vue`**

```vue
<script setup lang="ts">
import { computed } from 'vue';
import { pageRefsEqual } from '@embedpdf/vue/runtime';
import type { PageContextValue } from '@embedpdf/vue/runtime';
import type { Dot } from './dots';

const props = defineProps<{ page: PageContextValue; dots: readonly Dot[] }>();

const placed = computed(() =>
  props.dots
    .filter((dot) => pageRefsEqual(dot.page, props.page.ref))
    .map((dot) => ({ id: dot.id, color: dot.color, at: props.page.transform.toPixels(dot.point) })),
);
</script>

<template>
  <span
    v-for="dot in placed"
    :key="dot.id"
    class="dot"
    :style="{ left: `${dot.at.x}px`, top: `${dot.at.y}px`, background: dot.color }"
  />
</template>
```

**`dots.ts`**

```typescript
import type { PageRef } from '@embedpdf/vue/runtime';

export const COLORS = ['#e5484d', '#2f80ed', '#30a46c', '#1a2748'];

// A pen in the current color, with its tip at the bottom left.
export const penIcon = (color: string) =>
  `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
    <path d="M3 21l1.6-5.6L16.2 3.8a2.2 2.2 0 0 1 3.1 3.1L7.7 18.5z" fill="${color}" stroke="#fff" stroke-width="1.5" stroke-linejoin="round"/>
  </svg>`;

export interface Dot {
  readonly id: number;
  readonly page: PageRef;
  readonly point: { readonly x: number; readonly y: number };
  readonly color: string;
}
```

**`Dots.vue`**

```vue
<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';
import { Stage } from '@embedpdf/vue/stage';
import { RenderLayer } from '@embedpdf/vue/render';
import { useInteraction } from '@embedpdf/vue/interaction';
import DotLayer from './DotLayer.vue';
import PenCursor from './PenCursor.vue';
import { COLORS } from './dots';
import type { Dot } from './dots';

const interaction = useInteraction();
const color = ref(COLORS[0]);
const penCursor = ref(true);
const dots = ref<readonly Dot[]>([]);

// The tool reads the color at the moment of the click.
let remove: (() => void) | undefined;
onMounted(() => {
  remove = interaction.registerTool({
    id: 'dot',
    cursor: 'crosshair',
    onPointerDown: ({ page, point }) => {
      dots.value = [...dots.value, { id: dots.value.length + 1, page, point, color: color.value }];
      return true;
    },
  });
  interaction.activateTool('dot');
});
onUnmounted(() => remove?.());
</script>

<template>
  <PenCursor v-if="penCursor" :color="color" />
  <div class="toolbar">
    <div class="swatches" role="radiogroup" aria-label="Color">
      <button
        v-for="swatch in COLORS"
        :key="swatch"
        type="button"
        role="radio"
        :aria-checked="swatch === color"
        :aria-label="swatch"
        class="swatch"
        :style="{ background: swatch }"
        @click="color = swatch"
      />
    </div>
    <label class="check">
      <input v-model="penCursor" type="checkbox" />
      Pen cursor
    </label>
  </div>
  <Stage class="stage">
    <template #page="{ page }">
      <RenderLayer />
      <DotLayer :page="page" :dots="dots" />
    </template>
  </Stage>
</template>
```

**`PenCursor.vue`**

```vue
<script setup lang="ts">
import { useToolCursor } from '@embedpdf/vue/interaction';
import { penIcon } from './dots';

// While this is mounted, the dot tool's crosshair is a pen in `color`.
const props = defineProps<{ color: string }>();

useToolCursor(() => ({
  toolId: 'dot',
  cursors: { crosshair: { svg: penIcon(props.color), hotspot: { x: 3, y: 21 } } },
}));
</script>

<template></template>
```

`cursors` replaces the tool's cursors by name. Pass a function, and the cursor follows `color`; the
tool's own comes back when the component unmounts.

See [The cursor](../annotations/tools#the-cursor) for annotation tools.

## Touch

On a touch screen, the viewer behaves like a native app:

- **One finger scrolls**, and two fingers pinch-zoom, with `pointer` and `pan`.
- **Drawing tools take one finger**: it draws, and two fingers still scroll and zoom.
- **Click tools**, such as placing a note, place with a tap, and a drag still scrolls.
- **Long-press selects a word**, and handles grow the selection.

A tool of your own chooses with `touch`: `'draw'` for tools you drag with, `'tap'` for tools you
click with.

## Haptic feedback

Register a feedback provider, and a long-press that lands on a word gives the small tick people know
from native apps:

**`haptics.vue`**

```vue
<script setup lang="ts">
import { feedbackPlugin, interactionPlugin, vibrationFeedback } from '@embedpdf/vue/interaction';
import { Viewer } from '@embedpdf/vue/runtime';
import { engine } from './pdf';

const plugins = [/* … */ interactionPlugin(), feedbackPlugin({ provider: vibrationFeedback })];
</script>

<template>
  <Viewer :engine :plugins>
    <!-- your toolbar and pages -->
  </Viewer>
</template>
```

It's safe to leave in: where the browser can't vibrate, iOS Safari included, nothing happens. In a
native app shell, pass `wkFeedback('yourHandlerName')` to use the app's own haptics.

## Methods

Everything `useInteraction()` gives you.

| Method                                                        | What it does                                                                                                                                                                          |
| :------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `activateTool(id)`                                            | Makes a tool the active one. An id that isn't registered is refused. [The built-in tools](#the-built-in-tools)                                                                        |
| `pushTool(id)`, `popTool()`                                   | Switches to a tool and remembers the one before; goes back to it. [Hold a tool for a moment](#hold-a-tool-for-a-moment)                                                               |
| `activateDefaultTool()`                                       | Goes back to the default tool                                                                                                                                                         |
| `getDefaultToolId()`                                          | The tool a document opens with, `pointer` unless you set another                                                                                                                      |
| `registerTool(tool, { replace? })`                            | Adds a tool of your own, and returns a function that removes it. An id that's taken is refused unless you pass `replace: true`. [Your own tool](#your-own-tool)                       |
| `setToolCursor(id, cursors)`                                  | Replaces a tool's cursors by name while the app runs; `null` brings its own back. In a component, use `useToolCursor()`, which undoes it when the component goes. [Cursors](#cursors) |
| `getSettings()`, `updateSettings(changes)`, `resetSettings()` | Reads the settings, changes them while the app runs, or goes back to what you registered. In a component, use `useInteractionSettings()`. [Settings](#settings)                       |

## State

Everything `useInteractionState()` gives you, as refs: your template updates when a value changes. The method in the last column reads the same value once.

| Field          | What it is                                                                          | Read once                                                    |
| :------------- | :---------------------------------------------------------------------------------- | :----------------------------------------------------------- |
| `activeToolId` | The active tool's id                                                                | `getActiveToolId()`, or `getActiveTool()` for the whole tool |
| `tools`        | Every tool you can switch to, in the order they were added, the built-in ones first | `listTools()`, or `getTool(id)` and `hasTool(id)` for one    |

## Settings

Set them when you register the plugin, and change them while the app runs with
`interaction.updateSettings({ … })`; `interaction.resetSettings()` goes back to what you registered, and
`useInteractionSettings()` gives you the current settings.

| Setting       | What it does                                 | Default     |
| :------------ | :------------------------------------------- | :---------- |
| `defaultTool` | The tool that's active when a document opens | `'pointer'` |
| `tools`       | Your own tools, registered from the start    | none        |

## Events

**`tool-events.vue`**

```vue
<script setup lang="ts">
import { useInteractionEvent } from '@embedpdf/vue/interaction';
import { analytics } from './analytics';

useInteractionEvent(
  (interaction) => interaction.onToolChanged,
  ({ toolId }) => analytics.track('tool', { toolId }),
);
</script>

<template>
  <!-- your toolbar and pages -->
</template>
```

| Event                | Fires when                                       | Carries                         |
| :------------------- | :----------------------------------------------- | :------------------------------ |
| `onToolChanged`      | Another tool became active                       | `toolId`, `previousToolId`      |
| `onGestureStarted`   | A drag with a tool began                         | `toolId`, `page`, `pointerType` |
| `onGestureEnded`     | It ended                                         | `toolId`, `page`, `pointerType` |
| `onGestureCancelled` | It was cancelled, for example by a second finger | `toolId`, `page`, `pointerType` |
| `onSettingsChanged`  | The settings changed                             | `settings`, `changed`           |

## Good to know

### Each document has its own tool

The active tool belongs to the document: switch tabs, and each document is in the tool you left it in.

### Permissions

Tools don't check permissions themselves; the features behind them do. A drawing tool without
permission to create draws nothing. Hide the buttons with each plugin's checks, such as
`annotation.canCreate()`.
