# Text

> Text components measure with the font files of the video's media, so a layout is the same in the editor and in the render.

Page: https://fframes-kit.pages.dev/docs/text-and-formulas/text/

Text components measure with the font files of the video's media, so a layout is the same
in the editor and in the render. When a family cannot be measured (a system font, or the
editor before fonts load), each falls back to a plain `<text>`, as listed. `text` is a
`Cow<str>`: pass `"literal".into()` or `format!(..).into()`. `style` is a `TextStyle` from
`theme.typography`, and its `family`, `size` and `weight` are what is measured and drawn.
`anchor` is `TextAnchor::{Start, Middle, End}`.

| Component | Props | What it does | Without a font |
| --- | --- | --- | --- |
| `FitText` | `text`, `x`, `y`, `style`, `max_width`, `min_size` (24), `step` (4), `fill`, `anchor`, `opacity` | one line at the largest size up to `style.size` that fits `max_width`; `fit.size(ui)` gives the size | `style.size` |
| `Letters` | `text`, `x`, `y`, `style`, `fill`, `tracking`, `look: Box<dyn Fn(usize) -> LetterLook>` | each letter moved, faded and scaled about its center by `look(i)` (`LetterLook { dx, dy, opacity, scale }`) | one line with `look(0)` |
| `TextReveal` | `text`, `x`, `y`, `style`, `fill`, `by` (`RevealBy::Word`, `Letter`), `shift`, `delay`, `stagger`, `run_time`, `rate_func` | words or letters fade in one after another while they move by `shift` into place | the whole line as unit 0 |
| `Typewriter` | `text`, `x`, `y`, `style`, `fill`, `delay`, `chars_per_second` (24), `cursor: Option<Color>`, `cursor_width`, `blink` (0.5 s) | types one character at a time, with a cursor that blinks once the text is complete | no cursor |
| `Counter` | `from`, `to`, `x`, `y`, `style`, `fill`, `anchor` (`End`), `decimals`, `thousands` (true), `prefix`, `suffix`, `delay`, `run_time`, `rate_func` | counts from `from` to `to`; right aligned, so the digits stay in place as the number grows | the same |
| `Paragraph` | `text`, `x`, `y`, `width`, `style`, `fill`, `align: TextAlign`, `opacity` | wraps into lines of at most `width` pixels, `style.line_height` apart; `\n` starts a line | one unwrapped line |
| `HudLabel` | `text`, `x`, `y`, `style` (caption), `fill` (muted), `anchor`, `letter_spacing` (2.5), `uppercase` (true), `opacity` | a small tracked label, like `01 / RENDER` | the same |
| `Title` | `text`, `x`, `y`, `style`, `fill`, `include_underline` (true), `match_underline_width_to_text` (false), `underline_width` (1720), `underline_buff`, `underline_stroke`, `underline_stroke_width`, `opacity` | a centered title with an optional underline | fixed underline width |

Gallery scenes: `Letters`, `TextReveal`, `Typewriter`, `Counter`, `Paragraph`, `HudLabel`, `Title`.

Gallery clip of `Letters`: https://fframes-kit.pages.dev/media/scenes/Letters-ef6be6a5.mp4

Gallery clip of `TextReveal`: https://fframes-kit.pages.dev/media/scenes/TextReveal-6526ed72.mp4

Gallery clip of `Typewriter`: https://fframes-kit.pages.dev/media/scenes/Typewriter-b0fc3a72.mp4

Gallery clip of `Counter`: https://fframes-kit.pages.dev/media/scenes/Counter-bfc37422.mp4

Gallery clip of `Paragraph`: https://fframes-kit.pages.dev/media/scenes/Paragraph-41491469.mp4

Gallery clip of `HudLabel`: https://fframes-kit.pages.dev/media/scenes/HudLabel-e8a6bedd.mp4

Gallery clip of `Title`: https://fframes-kit.pages.dev/media/scenes/Title-c0476731.mp4

Fit a line, type another and draw an underlined title:

```rust
use fframes_kit::prelude::*;

fn typing<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
    let fitted = ui.show(FitText { text: "A title that fits".into(), x: 200.0, y: 300.0, max_width: 600.0, ..ui.defaults() });
    let typed = ui.show(Typewriter { text: "Typed, then erased".into(), x: 200.0, y: 500.0, chars_per_second: 16.0, erase: Some(0.25), ..ui.defaults() });
    let title = ui.show(Title { text: "An underlined title".into(), y: 700.0, match_underline_width_to_text: true, ..ui.defaults() });
    svgr!(<g>{fitted}{typed}{title}</g>)
}
```

Gallery scene: `Label`.

Gallery clip of `Label`: https://fframes-kit.pages.dev/media/scenes/Label-68b0cfd5.mp4

`Label` positions text next to a point. Its `direction` is an `Anchor`, and `buff` is the
distance from the point in pixels.

```rust
use fframes_kit::prelude::*;

fn label<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
    ui.show(Label {
        text: "Right".into(), x: 960.0, y: 600.0,
        direction: Anchor::Right, buff: 35.0, ..ui.defaults()
    })
}
```

```rust
use fframes_kit::prelude::*;

/// A stat: a label, then the number counting up under it.
fn stat<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
    let label = ui.show(HudLabel { text: "frames rendered".into(), x: 96.0, y: 400.0, ..ui.defaults() });
    let number = ui.show(Counter {
        to: 12_480.0,
        x: 96.0,
        y: 520.0,
        anchor: TextAnchor::Start,
        delay: 0.2,
        ..ui.defaults()
    });
    let title = ui.show(TextReveal { text: "Twelve thousand frames".into(), x: 96.0, y: 700.0, ..ui.defaults() });
    svgr!(<g>{label}{number}{title}</g>)
}

let c = Counter { to: 1_000.0, decimals: 1, suffix: " ms".into(), ..Counter::default() };
assert_eq!(c.format(c.value(100.0)), "1,000.0 ms");
```

`Letters` takes any per-letter animation. Here each letter drops in with a spring, 60 ms
after the one before:

```rust
use fframes_kit::prelude::*;

fn drop_in<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
    let t = ui.seconds();
    ui.show(Letters {
        text: "LAUNCH".into(),
        x: 96.0,
        y: 600.0,
        look: Box::new(move |i| {
            let s = motion::spring(t - motion::stagger(i, 0.06), 260.0, 18.0);
            LetterLook { dy: (1.0 - s) * -120.0, opacity: motion::clamp01(s * 2.0), ..LetterLook::default() }
        }),
        ..ui.defaults()
    })
}
```

### Fitting text into lines

These are Remotion's `@remotion/layout-utils` text fitters, measured like the components
above. Both return `None` when the family cannot be measured; draw the text at `style.size`
then.
Gallery scene: `FitText`.

Gallery clip of `FitText`: https://fframes-kit.pages.dev/media/scenes/FitText-1aa030fc.mp4

- **`text::fit_text_on_n_lines(ui, text, style, max_width, max_lines)`** gives
  `FitLines { size, lines }`: the largest size up to `style.size` at which `text` breaks at
  spaces into at most `max_lines` lines of `max_width` pixels. Each line is measured whole,
  so none is wider than `max_width`. It is also `None` when a single word is wider than the
  box at size 1.
- **`text::fill_text_box(max_width, max_lines)`** is an empty `TextBox`.
  - `add(ui, style, " word")` puts a word on the last line, or starts a new line for it.
  - It returns `BoxFit { exceeds_box, new_line }`.
  - A word that does not fit is not added.
  - `lines()` gives the lines so far.

```rust
use fframes_kit::{prelude::*, text::fit_text_on_n_lines};

/// A quote as large as it can be on three lines of 1400 pixels.
fn quote<'a>(ui: &mut Ui<'a, '_, '_>, text: &'a str) -> Svgr<'a> {
    let style = ui.theme.typography.title;
    let Some(fit) = fit_text_on_n_lines(ui, text, style, 1400.0, 3) else {
        return ui.show(Paragraph { text: text.into(), x: 260.0, y: 400.0, width: 1400.0, ..ui.defaults() });
    };
    let line_height = fit.size as f32 * style.line_height;
    let lines: Vec<Svgr<'a>> = fit
        .lines
        .into_iter()
        .enumerate()
        .map(|(i, line)| {
            let y = 400.0 + i as f32 * line_height;
            svgr!(<text x="260" y={y} font-family={style.family} font-size={fit.size} font-weight={style.weight} fill="#fff">{line}</text>)
        })
        .collect();
    svgr!(<g>{lines}</g>)
}
```

### Numbers and Manim's text mobjects

| Component | Props | What it does | Without a font |
| --- | --- | --- | --- |
| `DecimalNumber` | `value: ValueTracker`, `x`, `y`, `style` (title), `fill`, `anchor` (`Start`), `num_decimal_places` (2), `include_sign`, `group_with_commas` (true), `unit`, `opacity` | the value with every digit in a slot as wide as the widest digit, so digits do not jitter; `anchor: End` keeps them in place across a carry | one line |
| `Integer` | as `DecimalNumber` without `num_decimal_places` | the value rounded to a whole number | one line |
| `Variable` | `label`, `value`, `x`, `y`, `style` (heading), `fill`, `value_fill` (accent), `num_decimal_places` (2), `unit`, `opacity` | `label = value`, the label in place and the number after it | one line |
| `BulletedList` | `items: Vec<Cow<str>>`, `x`, `y`, `style` (body), `fill`, `bullet_fill` (accent), `indent`, `line_spacing`, `opacity`, `delay` (0), `stagger` (`None`), `run_time` (enter) | one line per item after a round bullet (a circle, no glyph needed); `baseline(i)` gives a line's `y`. With `stagger: Some(s)` item `i` fades and rises in from `delay + i * s` seconds over `run_time`, and `revealed_at(i)` gives when it is in | the same |
| `Title` | `text`, `x` (960), `y` (130), `style`, `fill`, `include_underline` (true), `match_underline_width_to_text`, `underline_width` (1720), `underline_buff`, `underline_stroke`, `underline_stroke_width`, `opacity` | a centered heading over an underline | the underline `underline_width` wide |
| `MarkupText` | `markup`, `x`, `y`, `style` (heading), `fill`, `anchor`, `opacity` | one line with Pango tags: `<b>`, `<i>`, `<u>`, `<span>` with `foreground`, `size` (pixels or `xx-small` to `xx-large`), `weight`, `style`, `underline` | the same |

`value` is a `ValueTracker`: a fixed number (`3.5.into()`), a stored `timeline!`
(`(&self.price).into()`), or a value computed from time in `render_frame`
(`f(ui.seconds()).into()`), which is Manim's `always_redraw`. It is an `f32`, exact to about
seven significant digits.

Gallery scenes: `DecimalNumber`, `Integer`, `Variable`, `BulletedList`, `MarkupText`.

Gallery clip of `DecimalNumber`: https://fframes-kit.pages.dev/media/scenes/DecimalNumber-6b83f8b7.mp4

Gallery clip of `Integer`: https://fframes-kit.pages.dev/media/scenes/Integer-2a84ecd9.mp4

Gallery clip of `Variable`: https://fframes-kit.pages.dev/media/scenes/Variable-dd4aa3ee.mp4

Gallery clip of `BulletedList`: https://fframes-kit.pages.dev/media/scenes/BulletedList-72a8ff17.mp4

Gallery clip of `MarkupText`: https://fframes-kit.pages.dev/media/scenes/MarkupText-f498668c.mp4

```rust
use fframes_kit::prelude::*;

fn readout<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
    let t = ui.seconds();
    let title = ui.show(Title { text: "Free fall".into(), ..ui.defaults() });
    let height = ui.show(Variable {
        label: "h".into(),
        value: (100.0 - 4.905 * t * t).max(0.0).into(),
        x: 200.0,
        y: 400.0,
        unit: " m",
        ..ui.defaults()
    });
    let steps = ui.show(BulletedList {
        items: vec!["drop the ball".into(), "read the height".into()],
        x: 200.0,
        y: 560.0,
        ..ui.defaults()
    });
    let note = ui.show(MarkupText {
        markup: "<b>no</b> air <span foreground='#ff5500'>resistance</span>".into(),
        x: 200.0,
        y: 760.0,
        ..ui.defaults()
    });
    svgr!(<g>{title}{height}{steps}{note}</g>)
}

let n = DecimalNumber { include_sign: true, unit: " m", ..DecimalNumber::default() };
assert_eq!(n.format(1234.5), "+1,234.50 m");
```

An integer that counts frames at 30 fps and a list that reveals one item every half second:

```rust
use fframes_kit::prelude::*;

fn count<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
    ui.show(Integer {
        value: (ui.seconds() * 30.0).into(),
        x: 960.0,
        y: 560.0,
        anchor: TextAnchor::Middle,
        unit: " frames",
        ..ui.defaults()
    })
}

fn list<'a>(ui: &mut Ui<'a, '_, '_>) -> Svgr<'a> {
    ui.show(BulletedList {
        items: vec!["Choose a scene".into(), "Animate its parts".into(), "Render the video".into()],
        x: 300.0,
        y: 400.0,
        style: ui.theme.typography.heading,
        delay: 0.2,
        stagger: Some(0.5),
        ..ui.defaults()
    })
}
```

Bold and italic in `MarkupText` draw with the family's bold and italic faces, so put those
font files into the media. Unknown tags and attributes are drawn without their style, and
markup that is not well formed is drawn as plain text; `inspect` warns about both.
`inspect --pops` reports changing digits as pops "like a counter"; that is the number
counting, not a layout jump.

## API

Components are plain structs. Set the fields you need and fill the rest with `..ui.defaults()`: the defaults below come from the current theme (`theme.` values are fields of `Theme`).

### FitText

One line of text at the largest size, up to `style.size`, that fits into `max_width` pixels. It shrinks by `step` pixels down to `min_size`, where it stays even if it is still too wide. Without a measurable font it keeps `style.size`.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The words, on one line. |
| `x` | `f32` | `0.0` | The anchor point on the baseline. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.title` | The font: its size in pixels is the largest size tried; family and weight are kept. |
| `max_width` | `f32` | `1600.0` | Widest the line may be, in pixels. |
| `min_size` | `usize` | `24` | Smallest font size in pixels it shrinks to, even if the text is still too wide. |
| `step` | `usize` | `4` | Pixels the font size shrinks by at each try (at least 1). |
| `fill` | `Color` | `theme.colors.title` | Colour of the text. |
| `anchor` | `TextAnchor` | `TextAnchor::Start` | Which point of the text sits at `x`: start, middle or end. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |

Methods:

- `fn size(&self, ui: &Ui) -> usize`: The font size the text is drawn at.

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/fit.rs#L17)

### Letters

A line of text drawn letter by letter, each letter with its own `LetterLook` from `look(index)`. `(x, y)` is the start of the baseline and `tracking` adds pixels after every letter. Letters at opacity 0 are left out. Without a measurable font the line is one `<text>` with the look of letter 0.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The line of text; each character is drawn on its own. |
| `x` | `f32` | `0.0` | Left of the first letter, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.title` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.title` | Colour of the letters. |
| `tracking` | `f32` | `0.0` | Extra pixels after every letter. |
| `look` | `Box<dyn Fn(usize) -> LetterLook>` | `Box::new(\|_\| LetterLook::default())` | Gives the `LetterLook` of letter `index`, counting from 0; letters at opacity 0 are not drawn. |

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/letters.rs#L38)

### TextReveal

A line whose words or letters fade in one after another while they move by `shift` into place. Unit `i` starts `delay + i * stagger` seconds into the container and takes `run_time`. `(x, y)` is the start of the baseline. Without a measurable font the whole line fades in as unit 0.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The words or letters that fade in; the whole line is drawn as one when the font cannot be measured. |
| `x` | `f32` | `0.0` | Left of the line, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.title` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.title` | Colour of the text. |
| `by` | `RevealBy` | `RevealBy::Word` | Whether it reveals word by word or letter by letter. |
| `shift` | `(f32, f32)` | `(0.0, 24.0)` | Canvas units, `y` down: `(0.0, 24.0)` rises 24 units into place. |
| `delay` | `f32` | `0.0` | Seconds after the container's start before the first unit begins. |
| `stagger` | `f32` | `theme.motion.stagger.tight` | Seconds between the starts of two units. |
| `run_time` | `f32` | `theme.motion.duration.enter` | Seconds each unit takes to fade and move into place. |
| `rate_func` | `RateFunc` | `theme.motion.easing.enter` | Easing of each unit over its run time. |

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/reveal.rs#L30)

### Typewriter

Types its text one character at a time, `chars_per_second` from `delay` seconds into the container, with a cursor after the last character (Manim's `TypeWithCursor`). The cursor is steady while typing and blinks every `blink` seconds once the text is complete. With `erase` it deletes the text again at the same rate, from the end, finishing `erase` seconds before the end of the container (Manim's `UntypeWithCursor`). `(x, y)` is the start of the baseline. Without a measurable font there is no cursor.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The words that are typed. |
| `x` | `f32` | `0.0` | Left of the text, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.body` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.text` | Colour of the text. |
| `delay` | `f32` | `0.0` | Seconds after the container's start before typing begins. |
| `chars_per_second` | `f32` | `24.0` | Characters typed (and erased) per second. |
| `cursor` | `Option<Color>` | `Some(theme.colors.accent)` | `None` draws no cursor. |
| `cursor_width` | `f32` | `3.0` | Cursor width in pixels; its height is the font size. |
| `blink` | `f32` | `0.5` | Seconds the cursor is on, then off, after typing; 0 keeps it on. |
| `erase` | `Option<f32>` | `None` | Seconds before the end of the container when the erasing is done; `None` keeps the text. |

Methods:

- `fn typed(&self, t: f32) -> usize`: Characters typed `t` seconds into the container.
- `fn shown(&self, t: f32, end: Option<f32>) -> usize`: Characters shown `t` seconds into a container that ends at `end` seconds: typed, less the erased ones.
- `fn cursor_on(&self, t: f32) -> bool`: Whether the cursor shows `t` seconds into the container; it is steady while erasing.
- `fn cursor_on_until(&self, t: f32, end: Option<f32>) -> bool`: `cursor_on` in a container that ends at `end` seconds.

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/typewriter.rs#L20)

### Counter

A number that counts from `from` to `to` over `run_time` seconds, starting `delay` seconds into the container: `"0" -> "12,480"`. It is right aligned by default, so the digits stay in place while the number grows.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `from` | `f64` | `0.0` | The value shown before `delay`. |
| `to` | `f64` | `0.0` | The value reached after `run_time`. |
| `x` | `f32` | `0.0` | Position of the number's anchor, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.title` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.title` | Colour of the text. |
| `anchor` | `TextAnchor` | `TextAnchor::End` | Which point of the text sits at `x`: start, middle or end. |
| `decimals` | `usize` | `0` | Digits after the decimal point. |
| `thousands` | `bool` | `true` | Groups the digits by three with commas. |
| `prefix` | `Cow<str>` | `Cow::Borrowed("")` | Text written before the number, such as a currency sign; empty for none. |
| `suffix` | `Cow<str>` | `Cow::Borrowed("")` | Text written after the number, such as a unit; empty for none. |
| `delay` | `f32` | `0.0` | Seconds after the container's start before it begins to count. |
| `run_time` | `f32` | `theme.motion.duration.slow * 2.0` | Seconds the count takes. |
| `rate_func` | `RateFunc` | `theme.motion.easing.standard` | Easing of the count over its run time. |

Methods:

- `fn value(&self, t: f32) -> f64`: The value `t` seconds into the container.
- `fn format(&self, value: f64) -> String`: The text shown for `value`.

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/counter.rs#L19)

### Paragraph

Text wrapped into lines of at most `width` pixels (`Frame::text_break_lines`), aligned inside that width. `(x, y)` is the start of the first baseline; lines are `style.line_height` apart and `\n` starts a new one. Without a measurable font it is one unwrapped line.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The words; `\n` starts a new line. |
| `x` | `f32` | `0.0` | Left edge of the wrap box in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the first line in pixels of the canvas; later lines go below by the line height. |
| `width` | `f32` | `800.0` | Width in pixels the text wraps into; lines are aligned inside it. |
| `style` | `TextStyle` | `theme.typography.body` | The font: family, size in pixels, weight and line height. |
| `fill` | `Color` | `theme.colors.text` | Colour of the text. |
| `align` | `TextAlign` | `TextAlign::Left` | How the lines sit inside `width`: left, center or right. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/paragraph.rs#L18)

### HudLabel

A small tracked label, like the annotations of an engineering drawing ("01 / RENDER"). `uppercase` turns the text to capitals.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The words; shown in capitals when `uppercase` is set. |
| `x` | `f32` | `0.0` | Position of the text's anchor point, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.caption` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.muted` | Colour of the text. |
| `anchor` | `TextAnchor` | `TextAnchor::Start` | Which point of the text sits at `x`: start, middle or end. |
| `letter_spacing` | `f32` | `2.5` | Pixels between letters. |
| `uppercase` | `bool` | `true` | `true` writes the text in capitals. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |

Methods:

- `fn draw(self) -> Svgr`: The label as a `<text>` element; it needs no `Ui`, so it can be drawn anywhere.

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/label.rs#L16)

### Title

Manim's `Title`: a line of text centered on `x` near the top, over an underline `underline_width` wide, or as wide as the text with `match_underline_width_to_text` (and a measurable font).

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The title words. |
| `x` | `f32` | `960.0` | Horizontal center of the title and its underline, in pixels of the canvas. |
| `y` | `f32` | `130.0` | The baseline. |
| `style` | `TextStyle` | `theme.typography.title` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.title` | Colour of the text. |
| `include_underline` | `bool` | `true` | `true` draws a line under the text. |
| `match_underline_width_to_text` | `bool` | `false` | `true` makes the underline as wide as the text instead of `underline_width` (when the font can be measured). |
| `underline_width` | `f32` | `1720.0` | Width of the underline in pixels. |
| `underline_buff` | `f32` | `28.0` | Pixels from the baseline down to the underline. |
| `underline_stroke` | `Color` | `theme.colors.title` | Colour of the underline. |
| `underline_stroke_width` | `f32` | `3.0` | Thickness of the underline in pixels. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/title.rs#L17)

### Label

One line of text whose box sits `buff` pixels from `(x, y)` in `direction`: `Top` puts it above the point, `TopRight` up and to the right, `Center` on it.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `text` | `Cow<str>` | `Cow::Borrowed("")` | The text, one line. |
| `x` | `f32` | `0.0` | X in pixels of the canvas of the point the text sits next to. |
| `y` | `f32` | `0.0` | Y in pixels of the canvas of the point the text sits next to (y down). |
| `direction` | `Anchor` | `Anchor::Right` | Which side of the point the text box sits on (`Top` above it, `TopRight` up and right, `Center` on it). |
| `buff` | `f32` | `theme.spacing.sm` | Pixels between the point and the text's box. |
| `style` | `TextStyle` | `theme.typography.body` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.title` | Colour of the text. |
| `enter` | `Option<FadeIn>` | `None` | Fade in at the start; `None` for none. |
| `exit` | `Option<FadeOut>` | `None` | Fade out before the end; `None` for none. |

Methods:

- `fn size(&self, ui: &Ui) -> (f32, f32)`: Width and height of the text's box, see `text_size`.
- `fn bounds(&self, ui: &Ui) -> Rect`: The box the text is drawn in, in pixels.

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/label.rs#L26)

### DecimalNumber

Manim's `DecimalNumber`: a number that follows a `ValueTracker`, drawn so its digits do not jitter while it changes. Every digit gets a slot as wide as the widest digit of the font, so a number keeps its width while its digit count stays the same, and with `anchor: End` the digits stay in place across a carry (`9.99 -> 10.00`). The value is an `f32`, exact to about seven significant digits.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `ValueTracker` | `0.0.into()` | The `ValueTracker` whose value is written; it is read at every frame. |
| `x` | `f32` | `0.0` | Position of the number's anchor, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.title` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.title` | Colour of the text. |
| `anchor` | `TextAnchor` | `TextAnchor::Start` | Which point of the number (with its unit) sits at `x`: start, middle or end. |
| `num_decimal_places` | `usize` | `2` | Digits after the decimal point. |
| `include_sign` | `bool` | `false` | A `+` before positive numbers. |
| `group_with_commas` | `bool` | `true` | `true` groups the digits by three with commas. |
| `unit` | `&str` | `""` | Text written after the number, such as `" kg"`; empty for none. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |

Methods:

- `fn format(&self, value: f64) -> String`: The text shown for `value`, with the unit.

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/decimal.rs#L50)

### Integer

Manim's `Integer`: a `DecimalNumber` without decimals, rounded to the nearest whole number.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `ValueTracker` | `d.value` | The `ValueTracker` whose value is written; it is read at every frame. |
| `x` | `f32` | `d.x` | Position of the number's anchor, in pixels of the canvas. |
| `y` | `f32` | `d.y` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `d.style` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `d.fill` | Colour of the text. |
| `anchor` | `TextAnchor` | `d.anchor` | Which point of the number (with its unit) sits at `x`: start, middle or end. |
| `include_sign` | `bool` | `d.include_sign` | `true` writes a `+` before positive numbers. |
| `group_with_commas` | `bool` | `d.group_with_commas` | `true` groups the digits by three with commas. |
| `unit` | `&str` | `d.unit` | Text written after the number, such as `" kg"`; empty for none. |
| `opacity` | `f32` | `d.opacity` | 0 transparent to 1 opaque. |

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/decimal.rs#L187)

### Variable

Manim's `Variable`: `label = value`, the value a `DecimalNumber` that follows a `ValueTracker`. `(x, y)` is the start of the label on the baseline; the label stays in place and the number grows to the right. For a value computed from time, pass `value: f(ui.seconds()).into()` in `render_frame`, which runs every frame.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `Cow<str>` | `Cow::Borrowed("")` | The name written before the ` = `. |
| `value` | `ValueTracker` | `0.0.into()` | The `ValueTracker` whose value is written; it is read at every frame. |
| `x` | `f32` | `0.0` | Left of the label, in pixels of the canvas; the number follows it. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.heading` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.text` | Colour of the label and the `=`. |
| `value_fill` | `Color` | `theme.colors.accent` | The color of the number; the label and `=` take `fill`. |
| `num_decimal_places` | `usize` | `2` | Digits after the decimal point. |
| `unit` | `&str` | `""` | Text written after the number, such as `" kg"`; empty for none. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/decimal.rs#L264)

### BulletedList

Manim's `BulletedList`: one line per item, each after a round bullet. `(x, y)` is the left of the first bullet on the first baseline; lines are `line_spacing` times the font size apart. The bullets are circles, so they need no glyph in the font.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `Vec<Cow<str>>` | `Vec::new()` | One line per item, each after a bullet. |
| `x` | `f32` | `0.0` | Left of the first bullet, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the first item, in pixels of the canvas. |
| `style` | `TextStyle` | `style` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.text` | Colour of the text. |
| `bullet_fill` | `Color` | `theme.colors.accent` | Colour of the bullets. |
| `indent` | `f32` | `style.size as f32 * 1.1` | Pixels from the bullet's left to the text. |
| `line_spacing` | `f32` | `style.line_height * 1.25` | Baseline distance as a multiple of the font size. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |
| `delay` | `f32` | `0.0` | Seconds before the first item comes in, when `stagger` is set. |
| `stagger` | `Option<f32>` | `None` | Seconds between the starts of two items coming in; `None` (the default) shows every item from the start. |
| `run_time` | `f32` | `theme.motion.duration.enter` | Seconds each item takes to fade and rise into place, when `stagger` is set. |

Methods:

- `fn baseline(&self, index: usize) -> f32`: The baseline of item `index`.
- `fn revealed_at(&self, index: usize) -> f32`: Seconds into the container when item `index` is fully in: 0 without a `stagger`.

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/bulleted_list.rs#L22)

### MarkupText

Manim's `MarkupText`: one line with Pango-style tags, `"a <b>bold</b> <span foreground='#ff5500'>word</span>"`.

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `markup` | `Cow<str>` | `Cow::Borrowed("")` | The text with Pango-style tags: `<b>`, `<i>`, `<u>` and `<span>`. |
| `x` | `f32` | `0.0` | Position of the text's anchor point, in pixels of the canvas. |
| `y` | `f32` | `0.0` | Baseline of the text, in pixels of the canvas. |
| `style` | `TextStyle` | `theme.typography.heading` | The font: family, size in pixels and weight. |
| `fill` | `Color` | `theme.colors.text` | Colour of the text outside any `<span>` colour. |
| `anchor` | `TextAnchor` | `TextAnchor::Start` | Which point of the text sits at `x`: start, middle or end. |
| `opacity` | `f32` | `1.0` | 0 transparent to 1 opaque. |

[Source](https://github.com/khanhthanhdev/fframes-kit/blob/main/fframes-kit/src/text/markup.rs#L29)
