# Handle events
React to dotLottie playback events from C by draining the event queue each frame, and read the frame number and loop count each event carries.

The runtime reports what the player is doing — loaded, started playing, completed a loop — through a queue you drain, rather than through callbacks you register.

## Why polling instead of callbacks

Callbacks across an FFI boundary mean handing a function pointer to the library and having it invoked from whatever thread and stack the library happens to be on. That constrains what you can safely do inside the handler and complicates lifetime management on both sides.

Queued events avoid all of it. Events accumulate as playback progresses, and you consume them at a point you choose, on your own thread, where your application state is already accessible.

## Draining the queue

`dotlottie_poll_event()` writes one event into a struct you provide and tells you whether it wrote anything:

| Return | Meaning                        |
| ------ | ------------------------------ |
| `1`    | An event was written           |
| `0`    | The queue is empty             |
| `-1`   | A null player or event pointer |

Drain it fully with a `while` loop, since more than one event can be queued per tick:

```c
dotlottieDotLottiePlayerEvent event;

while (dotlottie_poll_event(player, &event) == 1) {
  switch (event.event_type) {
    case Load:
      printf("animation loaded\n");
      break;
    case LoadError:
      fprintf(stderr, "animation failed to load\n");
      break;
    case Loop:
      printf("completed loop %u\n", event.data.loop_count);
      break;
    case Complete:
      printf("playback finished\n");
      break;
    default:
      break;
  }
}
```

Poll once per iteration of your render loop, after `dotlottie_tick()`. Events that are never polled stay queued and accumulate, so drain the queue even if you only care about a couple of event types.

## Event types

| Type        | Numeric | Fires when                              | Payload      |
| ----------- | ------- | --------------------------------------- | ------------ |
| `Load`      | 0       | An animation finished loading           | —            |
| `LoadError` | 1       | An animation failed to load             | —            |
| `Play`      | 2       | Playback started                        | —            |
| `Pause`     | 3       | Playback paused                         | —            |
| `Stop`      | 4       | Playback stopped and reset              | —            |
| `Frame`     | 5       | The current frame advanced              | `frame_no`   |
| `Render`    | 6       | A frame was drawn                       | `frame_no`   |
| `Loop`      | 7       | A loop iteration completed              | `loop_count` |
| `Complete`  | 8       | Playback finished and will not continue | —            |

<Callout type="warning" title="Event constants have no prefix">
  The values are emitted bare — `Load`, `Frame`, `Loop`, `Complete` — not `DotLottiePlayerEventType_Load`. Some doc
  comments inside the generated header show the prefixed spelling, but those identifiers do not exist and will not
  compile. Because names this common share the global namespace, watch for collisions with your own macros and enums.
</Callout>

## Reading the payload

Two event types carry data, in a union whose active member is determined by the event type:

```c
switch (event.event_type) {
  case Frame:
  case Render:
    printf("frame %.2f\n", event.data.frame_no);   /* float */
    break;
  case Loop:
    printf("loop %u\n", event.data.loop_count);    /* uint32_t */
    break;
  default:
    break;                                         /* no payload */
}
```

Reading `frame_no` on a `Loop` event, or `loop_count` on a `Frame` event, reinterprets unrelated bytes and yields nonsense. Always branch on `event_type` first.

`Frame` and `Render` fire every time the animation advances or draws — many times per second. Logging from those handlers will flood your output.

## Where events fit in the loop

```c
double last = now_seconds();

for (;;) {
  double current = now_seconds();
  float dt = (float)(current - last);
  last = current;

  bool rendered = false;
  dotlottie_tick(player, dt, &rendered);

  dotlottieDotLottiePlayerEvent event;
  while (dotlottie_poll_event(player, &event) == 1) {
    handle_event(&event);
  }

  if (rendered) {
    present(buffer, width, height);
  }
}
```

For the common cases you don't need events at all — `dotlottie_is_complete()`, `dotlottie_status()`, and `dotlottie_get_current_loop_count()` answer the same questions by direct query. Events are most useful when you want to act at the exact moment something changes, such as starting a sound on `Loop` or advancing UI state on `Complete`.

## State machine events

State machines have their own separate queues, polled with `dotlottie_state_machine_poll_event()` and `dotlottie_state_machine_poll_internal_event()`. Their events report transitions and input changes rather than playback progress, and unlike player events they contain string pointers with a limited lifetime. See [State machines](/en/runtimes/distributions/native/v0.x/state-machines) for details.

## Learn more

- [Control playback](/en/runtimes/distributions/native/v0.x/playback-control) — the tick loop events hang off
- [Memory and safety rules](/en/runtimes/distributions/native/v0.x/memory-and-safety) — event string lifetimes
- [C API reference](/en/runtimes/distributions/native/v0.x/api-reference) — full signatures
