# Graphics & rendering

AURA uses SDL3 for windows and input and Skia for 2D graphics and text. Its C++ scene graph connects layout, interaction, animation and media.

## Graphics foundation

- C++20 and CMake provide native integration.
- Skia draws rounded rectangles, text through HarfBuzz/ICU, SVGs and images.
- Native GPU output and CPU-based offscreen rendering share the same scene graph.
- Libraries, versions and license texts are listed in [Dependencies & licenses](dependencies.md).

## Structure

| File | Contents |
|---|---|
| `application.h` | Window, GL context, Skia GPU surface, event and rendering loop, offscreen PNG |
| `node.h` | Scene graph: `Node`, `RectNode`, `ImageNode`, `SvgNode`, `TextNode`, `ScrollNode` |
| `layout.h` | QML-like anchors (`place(node).left(44).below(title, 30)`), `gridCell` for grids |
| `animation.h` | Tweens with easing (Linear, InOutCubic, InOutQuint, OutQuad, OutBounce); bound to the target node, they stop when it is deleted |
| `resources.h` | Cache for images, SVGs and application-provided fonts |
| `video.h` | `VideoPlayer` (WebM/VP9, without audio, looping playlist) and `VideoNode` |

- **Layouts:** AURA now supports `Column`, `Row` and `Grid` in addition to explicit positions and anchors. See the [language reference](language.md) for details.
- **Logical resolution:** `WindowOptions` defines the logical size; the scene is scaled into the window for display.
- **Input:**
  - Tapping via `onTap`
  - Dragging after 10 px on the nearest `ScrollNode`
  - Flicking with deceleration and spring-back, similar to `Flickable`
    (2500 px/s maximum, 1500 px/s² deceleration)
- **Clipping:** `clip` clips to a rectangle. Images can use `radius` and `roundedCorners` for their own rounded edges.
- **On-demand rendering:** without an animation, the loop sleeps in
  `SDL_WaitEventTimeout`.

## Layout (`aura/ui/layout.h`)

QML anchors without bindings: a node is positioned once relative to its parent or
a sibling node (same parent).

```qml
Text {
    id: title
    text: "AURA"
    pixelSize: 28
    place: left 44, top 44
}
Text {
    id: subtitle
    text: "Application UI Runtime Architecture"
    pixelSize: 18
    place: left 44, below title 30
}
```

| Call | Meaning |
|---|---|
| `left/right/top/bottom(m)` | Distance to the parent's edge (`right`/`bottom` require the node's own size) |
| `centerX/centerY(offset)`, `center()` | Centered in the parent, rounded to whole pixels like the old layouts |
| `below/above/rightOf/leftOf(sibling, spacing)` | Next to a sibling node |
| `alignLeft/alignTop/alignBottom(sibling)`, `centerXOn/centerYOn(sibling)` | Aligned to a sibling node |
| `size(w, h)` | Set size (especially before `right`, `bottom`, and `center`) |

- Order matters: set the size first, then center or align to the right/bottom.
  The parent must already have its size at that point.
- Groups get their own container node, or an `Item` in AURA.
- `gridCell(index, columns, w, h, spacingX, spacingY)` returns a cell in a grid
  filled row by row (QML `Grid`).
- `Text` derives its natural size from the content; `wrap`, `fit` and `Context::fontSize()` control wrapping and font sizes.
- `Node::pressPosition` is the press location (for ripples).
  `Animation::onFinished` chains animations.
- Buttons can use `Button` or custom components; `pressSink` adds pressed feedback.
- **Input:** `aura::ui::InputDriver` (`input.h`) acts as the finger on a scene:
  pressed state, tapping when released over the same node, `onDrag`, and dragging
  the nearest `ScrollNode`. `Application` drives one with the mouse;
  `aura-view` uses its own.
- **Removal from a handler:** `node->removeLater()` immediately removes the node
  from the tree and deletes it before the next frame (`Node::deleteRemoved()`,
  called by `Application`); a button can therefore
  close its own dialog.

## Background work

- `aura::TaskRunner` (`aura/core/tasks.h`) runs jobs on a small pool (processor
  count minus one, between 2 and 4 threads), cancelable through `std::stop_token`;
  `stop(id)` also stops a job that is still waiting. `latest(slot, work, result)`
  handles tasks where only the newest result matters (order lookup, the displayed
  photo): a new job on the same `aura::Latest` stops the old one and discards its
  result, also after `cancel()` or when the slot no longer exists. Editor
  thumbnails are decoded as a separate job per photo, in parallel.
- Results return to the UI thread through `post()`: the single UI-thread queue
  (`aura::post()`), processed by the application loop before every frame
  (`aura::runPosted()`). Callbacks belonging to a destroyed `TaskRunner` are
  discarded.
- `post()` wakes the event loop through `Application::wakeUp()`. Without waking
  it, the result would arrive only at the 500-ms idle tick.
- The destructor stops and joins all jobs. Callbacks therefore never outlive
  their owner.

## Text

- Layout using SkParagraph and HarfBuzz/ICU.
- `TextNode` supports:
  - Alignment
  - `HorizontalFit`: reduces font size down to `minimumPixelSize`
  - Wrapping
  - Simple rich text (`<b>`, `<br>`, entities)
  - Styled runs (`setRuns`: weight, italic, color, size)
  - `lineHeight` as in QML (a multiplier of the font's line height). Qt places the extra spacing below each line. Skia distributes it,
    which is why `kDisableFirstAscent` and the adjustment in `implicitHeight()`
    exist.
- If a font is missing characters, Skia searches system fonts through fontconfig
  on Linux for a replacement.
- Surfaces use grayscale antialiasing **without** gamma and contrast enhancement
  (`SkSurfaceProps(0, kUnknown, 0, 1)`). With Skia's defaults, white text on red
  appears significantly bolder than in Qt.

## Video

- `VideoPlayer` demuxes with libwebm (mkvparser) and decodes with libvpx on a
  dedicated thread.
  - The queue holds 4 frames; the thread waits after that.
  - libvpx decodes with up to 4 threads.
- The UI thread maintains a clock per file and displays each frame when due.
  - Switching to the next file is a hard cut.
  - The playlist loops indefinitely.
- `VideoNode` uploads I420 planes as three A8 textures
  (`SkImages::TextureFromYUVAPixmaps`). The GPU converts to RGB (BT.601 limited).
  Without a GPU, for example in screenshots, a simple CPU routine converts them.
- The rendering loop is limited to 60 fps if the driver has no VSync.
  - With `--debug`, it logs fps every 5 s.
  - The player logs the number of displayed video frames per second.
- Achievable frame rates depend on the device, graphics driver, scene and video.

## Offscreen screenshots

`Application::renderToPng(root, width, height, file)` renders a scene to PNG without a window. The Hello example and gallery provide command-line entry points:

```bash
build/dev/examples/hello/aura-hello --render hello.png
build/dev/examples/demo/aura-demo --render-all /tmp/aura-gallery
build/dev/tools/aura-view/aura-view examples/hello --render /tmp/hello.png
```

See [aura-view](preview.md) for preview galleries and [Testing](testing.md) for visual comparisons.
