nandi/jolt-nativepublic Fork 0
5ae197f7be55bd41678978f11e41a4282ffcf4b7
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Bring vidya in cfd3e36 · on 5ae197f7be55bd41678978f11e41a4282ffcf4b7 · nandi · 19d ago
README.md · 233 lines · 11.5 KBmarkdown
Blame HistoryOpen raw

Vidya

GNOME/HIG-inspired theme layer for egui — no GTK.

Calm charcoal shells, a clear blue accent, soft borders, and a short type/spacing scale. Dark is the product default — ship apply_dark / Theme::dark() and stop there. Light (Theme::light / apply_light) exists for the showcase and for apps that explicitly need it; do not copy the demo’s dark/light toggle into every app.

Repo: tangled.org/nandi.uk/vidya

Screenshots below are Waydroid (portrait Android) captures of the demo APK.

Aesthetic

Layer Role
Window / view / card / popover Stacked surfaces (#242424#383838 in dark)
Accent Adwaita-like blue (#3584e4) for primary actions, selection, and checked controls
Feedback Destructive red, success green, warning amber
Type Title 20 · title₂ 16 · body 14 · caption 12
Spacing 4 · 6 · 12 · 18 · 24 · control height 34
Radius 6 · 9 · 12

apply() installs palette + spacing on the egui context so text fields, sliders, and combos inherit the shell. Checkboxes use a dedicated themed control (checkbox) with accent fill and a drawn checkmark. It also registers a tiny symbol font fallback (DejaVu subset) so UI punctuation such as , /, disclosure /, dashes, and curly quotes does not render as hollow boxes on Android.

Emoji → color icons: the full Twemoji 72×72 set (~3.8k glyphs) is embedded. Any reaction (flags, ZWJ sequences, skin tones) resolves via emoji_icon / paint_emoji_in / reaction_chip. Prefer these over raw Unicode on Android — egui’s default fonts only ship monochrome Noto Emoji.

On Android (edge-to-edge NativeActivity), call reserve_system_chrome(ctx, &theme) once per frame before other panels, or use top_header(ctx, &theme, |ui| { … }) for the app chrome. Prefer sync_system_chrome_from_android(ctx, &android_app) each frame so insets track content_rect (including when the soft keyboard is open). The IME reserve passes pointer events through so swipe typing on the system keyboard is not blocked.

Demo

An interactive showcase walks through overview, typography, actions, surfaces, palette swatches, and forms. The demo includes a dark/light toggle so you can compare palettes — that control is for the showcase, not a recommended app pattern.

raylib and Jolt port

The first native C/raylib port lives in raylib/, with
Jolt FFI bindings and a Jolt showcase in jolt/. The same ABI
is also implemented on this crate in ffi/, so Jolt can
render through the Rust/egui layer instead of the C reimplementation — same
symbols, same bindings, selected by library search path. The C ABI
keeps raylib types private, so Jolt and Zig callers only exchange primitive
values and UTF-8 strings. This is an independent backend; the established Rust
implementation remains available while controls and layout facilities are
ported incrementally.

glimmer

glimmer/ is a backend for
glimmer, Jolt's reactive toolkit — so
components written as hiccup, with reagent-style reactive cells, render as Vidya
widgets. egui has no widgets for a reconciler to patch, so the widget tree lives
in Rust behind a second C ABI
(ffi/include/vidya_tree.h) that the
Rust/egui build alone implements: the caller mutates integer node handles,
one call a frame paints the whole tree, and interactions come back as events.

nix run                  # apps.default → vidya-demo
nix run .#demo
# or from a remote flake:
nix run git+https://tangled.org/nandi.uk/vidya
nix develop            # rust (+ android target) · just · adb

just waydroid          # in-tree cargo apk → install → launch on Waydroid
just host              # desktop egui window
just install           # same as waydroid (rebuild APK)
just launch            # start installed activity only
just shots             # Waydroid screencaps → docs/screenshots/mobile/
just ffi               # buck2 → build/libvidya.so                (host)
just ffi-android       # buck2 → build/android/arm64-v8a/libvidya.so

Both are the same Rust/egui sources; only the target configuration moves.
Android is the sole backend on device now — the earlier cimgui/raylib
NativeActivity port is gone. just ffi-android needs ANDROID_NDK_HOME for the
linker (r27+); rustc and the aarch64-linux-android libstd are DotSlash-pinned
like everything else, so there is no rustup target to add.

just waydroid builds in android-demo/ with the nix develop toolchain (no temp/isolated copy).

Sections:

  • Overview — hero card, design tokens + grid DSL metrics table (demo can flip to light)
  • Typography — type scale samples (grid) and hierarchy
  • Actions — primary / default / destructive buttons, dialog footer, status pills
  • Surfaces — layer stack table, card & header frames
  • Palette — live semantic swatches (flip the shell to compare)
  • Forms — themed inputs, accent checkbox, slider, combo, progress

Refreshing screenshots

With a running Waydroid session:

just install   # rebuild + install APK
just shots     # adb screencap per section → docs/screenshots/mobile/

Use

Apps should start dark-only:

[dependencies]
vidya = { git = "https://tangled.org/nandi.uk/vidya" }
# or: vidya = { git = "ssh://git@tangled.org/nandi.uk/vidya" }
egui = "0.31"
use vidya::{apply_dark, checkbox, primary_button, Theme};

apply_dark(ctx);
let th = Theme::dark();
if primary_button(ui, &th, "Open").clicked() {
    // …
}
checkbox(ui, &th, &mut sync, "Sync preferences");

Theme::light() / apply_light are available when an app truly needs a light shell. Prefer not to expose a theme toggle unless that is a deliberate product choice (the Vidya demo does it to exercise both palettes).

Window icon

Embed a PNG and attach it to the viewport (set Wayland app_id to match your .desktop):

use egui::ViewportBuilder;
use vidya::with_app_icon_id;

let viewport = with_app_icon_id(
    ViewportBuilder::default().with_title("I/O Usage"),
    "usage",
    include_bytes!("../assets/usage-256.png"),
);

Nix flake

Output Role
apps.default / apps.demo Desktop showcase via cargo run (rustup + egui libs)
packages.default / packages.demo Same launcher + .desktop entry
packages.vidya Theme library sources + rlib bundle
devShells.default just · adb · egui runtime libs (rustup for cargo)
nix run                 # cargo run --manifest-path host/Cargo.toml
nix run .#demo
nix build .#vidya
nix develop

nix run keeps your cwd: from the checkout it rebuilds the live tree (same as just host). The packaged .desktop falls back to the flake source with CARGO_TARGET_DIR under ~/.cache/vidya/.

As a flake input:

inputs.vidya.url = "git+https://tangled.org/nandi.uk/vidya";

API

Item Role
Theme::dark() / light() Palette + spacing + type scale (dark for apps; light optional)
apply / apply_dark / apply_light Install on egui::Context (+ symbol font); prefer apply_dark
install_symbol_font Fallback glyphs for arrows / disclosure triangles / bullets / quotes
emoji_icon / paint_emoji_in / has_emoji_icon Full Twemoji color set by codepoint
Icon / icon / paint_icon_in Named shortcuts + stroke Plus / Copy
icon_button Square tool button with a stroke/emoji icon
reaction_chip Themed count chip with color emoji
primary_button / button / destructive_button Styled actions
checkbox Accent-filled checkbox with drawn checkmark
status_dot Live/offline circle (drawn — no Unicode tofu on Android)
text_field_singleline / text_field_multiline Text inputs with field padding (fill parent width)
consume_command / consume_escape Consume Cmd/Ctrl+key or Esc (platform-aware)
command_shortcut_label / escape_label "Ctrl+F" / "⌘F" / "Esc" for tooltips
title / title_2 / body / dim_label Text roles
Theme::header_frame / card_frame / page_frame Layout chrome
dialog Centered resizable window with card_frame (chain size / .show)
Theme::text_edit_margin Inner field padding (12×8 default)
Layout composition Prefer these over raw set_max_width / Layout plumbing
fit_width / fill_width Pin children to residual width (no edge overflow)
vstack Non-justified vertical stack (no giant gaps in tall parents)
card Fill-width themed card + vstack content
compact_card Fixed-width card that hugs content (gauge / anomaly tiles)
pack Wrap compact tiles without stretching leftover horizontal space
hflow Wrapping horizontal row (toolbars / chips)
lead_trail Leading field + trailing actions without clipping the actions
two_col / side_by_side Responsive two-column / stack (+ pure breakpoint policy)
page_body / central_page Enforced scrollable page: top-level content is a grid (GridCtx)
page_body_cols / central_page_cols Same, with explicit top-level ColSpecs
page_scroll Escape hatch: scroll + width pin only (no grid)
GridCtx::section Full-width page section (one row / one cell) — preferred page building block
inset_row Soft inset row capped to parent width
grid / grid_cols Grid DSL — pinned to residual width; cols share viewport budget
grid_cols_with / GridOpts Grid with striping / spacing options (page shell uses non-striped)
distribute_col_max Pure policy: per-column max widths so sum + gaps ≤ available
ColSpec Flex / Fixed / MetricBps / MetricRate (floors; capped by residual)
metric_cell Right-edge mono metric; clipped to column budget
RowDsl heading / text / dim / warn / metric / metric_bps / metric_rate
metric_bps / metric_rate Fixed-width monospace rate strings (no staircase columns)
metric_cell / table_metric / table_text Low-level cells (prefer the row DSL)
data_table Index-callback table helper on top of the grid DSL
reserve_system_chrome / system_chrome Android status + nav safe areas
sync_system_chrome_from_android Measured insets from AndroidApp::content_rect (Android)
top_header Header panel with system chrome already reserved
App icon Embed a PNG as the native window icon
icon_data_from_png Decode PNG bytes → egui::IconData
with_app_icon ViewportBuilder + embedded PNG icon
with_app_icon_id Same + Wayland app_id (match .desktop / StartupWMClass)
try_with_app_icon / try_with_app_icon_id Fallible variants

License

MIT

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# Vidya

GNOME/HIG-inspired **theme layer for [egui](https://github.com/emilk/egui)** — no GTK.

Calm charcoal shells, a clear blue accent, soft borders, and a short type/spacing scale. **Dark is the product default** — ship `apply_dark` / `Theme::dark()` and stop there. Light (`Theme::light` / `apply_light`) exists for the showcase and for apps that explicitly need it; do not copy the demo’s dark/light toggle into every app.

**Repo:** [tangled.org/nandi.uk/vidya](https://tangled.org/nandi.uk/vidya)

Screenshots below are **Waydroid** (portrait Android) captures of the demo APK.

<p align="center">
  <img src="docs/screenshots/mobile/vidya-home.png" alt="Vidya — Overview" width="320" />
  <img src="docs/screenshots/mobile/vidya-forms.png" alt="Vidya — Forms (themed checkbox)" width="320" />
  <img src="docs/screenshots/mobile/vidya-actions.png" alt="Vidya — Actions" width="320" />
</p>

<p align="center">
  <img src="docs/screenshots/mobile/vidya-colors.png" alt="Vidya — Palette" width="320" />
  <img src="docs/screenshots/mobile/vidya-type.png" alt="Vidya — Typography" width="320" />
  <img src="docs/screenshots/mobile/vidya-surfaces.png" alt="Vidya — Surfaces" width="320" />
</p>

## Aesthetic

| Layer | Role |
|-------|------|
| Window / view / card / popover | Stacked surfaces (`#242424``#383838` in dark) |
| Accent | Adwaita-like blue (`#3584e4`) for primary actions, selection, and checked controls |
| Feedback | Destructive red, success green, warning amber |
| Type | Title 20 · title₂ 16 · body 14 · caption 12 |
| Spacing | 4 · 6 · 12 · 18 · 24 · control height 34 |
| Radius | 6 · 9 · 12 |

`apply()` installs palette + spacing on the egui context so text fields, sliders, and combos inherit the shell. Checkboxes use a dedicated themed control (`checkbox`) with accent fill and a drawn checkmark. It also registers a tiny **symbol font fallback** (DejaVu subset) so UI punctuation such as `→`, `●`/`○`, disclosure `▾`/`▴`, dashes, and curly quotes does not render as hollow boxes on Android.

**Emoji → color icons:** the full **Twemoji** 72×72 set (~3.8k glyphs) is embedded. Any reaction (flags, ZWJ sequences, skin tones) resolves via `emoji_icon` / `paint_emoji_in` / `reaction_chip`. Prefer these over raw Unicode on Android — egui’s default fonts only ship monochrome Noto Emoji.

On **Android** (edge-to-edge NativeActivity), call `reserve_system_chrome(ctx, &theme)` once per frame **before** other panels, or use `top_header(ctx, &theme, |ui| { … })` for the app chrome. Prefer `sync_system_chrome_from_android(ctx, &android_app)` each frame so insets track `content_rect` (including when the soft keyboard is open). The IME reserve passes pointer events through so swipe typing on the system keyboard is not blocked.

## Demo

An interactive showcase walks through overview, typography, actions, surfaces, palette swatches, and forms. The demo includes a dark/light toggle so you can compare palettes — that control is for the showcase, not a recommended app pattern.

### raylib and Jolt port

The first native C/raylib port lives in [`raylib/`](raylib/README.md), with
Jolt FFI bindings and a Jolt showcase in [`jolt/`](jolt/README.md). The same ABI
is also implemented on **this** crate in [`ffi/`](ffi/README.md), so Jolt can
render through the Rust/egui layer instead of the C reimplementation — same
symbols, same bindings, selected by library search path. The C ABI
keeps raylib types private, so Jolt and Zig callers only exchange primitive
values and UTF-8 strings. This is an independent backend; the established Rust
implementation remains available while controls and layout facilities are
ported incrementally.

### glimmer

[`glimmer/`](glimmer/README.md) is a backend for
[glimmer](https://github.com/jolt-lang/glimmer), Jolt's reactive toolkit — so
components written as hiccup, with reagent-style reactive cells, render as Vidya
widgets. egui has no widgets for a reconciler to patch, so the widget tree lives
in Rust behind a second C ABI
([`ffi/include/vidya_tree.h`](ffi/include/vidya_tree.h)) that the
Rust/egui build alone implements: the caller mutates integer node handles,
one call a frame paints the whole tree, and interactions come back as events.

```bash
nix run                  # apps.default → vidya-demo
nix run .#demo
# or from a remote flake:
nix run git+https://tangled.org/nandi.uk/vidya
```

```bash
nix develop            # rust (+ android target) · just · adb

just waydroid          # in-tree cargo apk → install → launch on Waydroid
just host              # desktop egui window
just install           # same as waydroid (rebuild APK)
just launch            # start installed activity only
just shots             # Waydroid screencaps → docs/screenshots/mobile/
```

```bash
just ffi               # buck2 → build/libvidya.so                (host)
just ffi-android       # buck2 → build/android/arm64-v8a/libvidya.so
```

Both are the **same** Rust/egui sources; only the target configuration moves.
Android is the sole backend on device now — the earlier cimgui/raylib
NativeActivity port is gone. `just ffi-android` needs `ANDROID_NDK_HOME` for the
linker (r27+); rustc and the `aarch64-linux-android` libstd are DotSlash-pinned
like everything else, so there is no rustup target to add.

`just waydroid` builds **in `android-demo/`** with the nix develop toolchain (no temp/isolated copy).

Sections:

- **Overview** — hero card, design tokens + **grid DSL** metrics table (demo can flip to light)  
- **Typography** — type scale samples (grid) and hierarchy  
- **Actions** — primary / default / destructive buttons, dialog footer, status pills  
- **Surfaces** — layer stack table, card & header frames  
- **Palette** — live semantic swatches (flip the shell to compare)  
- **Forms** — themed inputs, accent checkbox, slider, combo, progress  

### Refreshing screenshots

With a running Waydroid session:

```bash
just install   # rebuild + install APK
just shots     # adb screencap per section → docs/screenshots/mobile/
```

## Use

Apps should start dark-only:

```toml
[dependencies]
vidya = { git = "https://tangled.org/nandi.uk/vidya" }
# or: vidya = { git = "ssh://git@tangled.org/nandi.uk/vidya" }
egui = "0.31"
```

```rust
use vidya::{apply_dark, checkbox, primary_button, Theme};

apply_dark(ctx);
let th = Theme::dark();
if primary_button(ui, &th, "Open").clicked() {
    // …
}
checkbox(ui, &th, &mut sync, "Sync preferences");
```

`Theme::light()` / `apply_light` are available when an app truly needs a light shell. Prefer not to expose a theme toggle unless that is a deliberate product choice (the Vidya demo does it to exercise both palettes).

### Window icon

Embed a PNG and attach it to the viewport (set Wayland `app_id` to match your `.desktop`):

```rust
use egui::ViewportBuilder;
use vidya::with_app_icon_id;

let viewport = with_app_icon_id(
    ViewportBuilder::default().with_title("I/O Usage"),
    "usage",
    include_bytes!("../assets/usage-256.png"),
);
```

## Nix flake

| Output | Role |
|--------|------|
| `apps.default` / `apps.demo` | Desktop showcase via **`cargo run`** (rustup + egui libs) |
| `packages.default` / `packages.demo` | Same launcher + `.desktop` entry |
| `packages.vidya` | Theme library sources + rlib bundle |
| `devShells.default` | just · adb · egui runtime libs (rustup for cargo) |

```bash
nix run                 # cargo run --manifest-path host/Cargo.toml
nix run .#demo
nix build .#vidya
nix develop
```

`nix run` keeps your cwd: from the checkout it rebuilds the live tree (same as `just host`). The packaged `.desktop` falls back to the flake source with `CARGO_TARGET_DIR` under `~/.cache/vidya/`.

As a flake input:

```nix
inputs.vidya.url = "git+https://tangled.org/nandi.uk/vidya";
```

## API

| Item | Role |
|------|------|
| `Theme::dark()` / `light()` | Palette + spacing + type scale (`dark` for apps; `light` optional) |
| `apply` / `apply_dark` / `apply_light` | Install on `egui::Context` (+ symbol font); prefer `apply_dark` |
| `install_symbol_font` | Fallback glyphs for arrows / disclosure triangles / bullets / quotes |
| `emoji_icon` / `paint_emoji_in` / `has_emoji_icon` | Full Twemoji color set by codepoint |
| `Icon` / `icon` / `paint_icon_in` | Named shortcuts + stroke Plus / Copy |
| `icon_button` | Square tool button with a stroke/emoji icon |
| `reaction_chip` | Themed count chip with color emoji |
| `primary_button` / `button` / `destructive_button` | Styled actions |
| `checkbox` | Accent-filled checkbox with drawn checkmark |
| `status_dot` | Live/offline circle (drawn — no Unicode tofu on Android) |
| `text_field_singleline` / `text_field_multiline` | Text inputs with field padding (fill parent width) |
| `consume_command` / `consume_escape` | Consume Cmd/Ctrl+key or Esc (platform-aware) |
| `command_shortcut_label` / `escape_label` | `"Ctrl+F"` / `"⌘F"` / `"Esc"` for tooltips |
| `title` / `title_2` / `body` / `dim_label` | Text roles |
| `Theme::header_frame` / `card_frame` / `page_frame` | Layout chrome |
| `dialog` | Centered resizable window with `card_frame` (chain size / `.show`) |
| `Theme::text_edit_margin` | Inner field padding (12×8 default) |
| **Layout composition** | Prefer these over raw `set_max_width` / `Layout` plumbing |
| `fit_width` / `fill_width` | Pin children to residual width (no edge overflow) |
| `vstack` | Non-justified vertical stack (no giant gaps in tall parents) |
| `card` | Fill-width themed card + `vstack` content |
| `compact_card` | Fixed-width card that **hugs content** (gauge / anomaly tiles) |
| `pack` | Wrap compact tiles without stretching leftover horizontal space |
| `hflow` | Wrapping horizontal row (toolbars / chips) |
| `lead_trail` | Leading field + trailing actions without clipping the actions |
| `two_col` / `side_by_side` | Responsive two-column / stack (+ pure breakpoint policy) |
| `page_body` / `central_page` | **Enforced** scrollable page: top-level content is a **grid** (`GridCtx`) |
| `page_body_cols` / `central_page_cols` | Same, with explicit top-level `ColSpec`s |
| `page_scroll` | Escape hatch: scroll + width pin only (no grid) |
| `GridCtx::section` | Full-width page section (one row / one cell) — preferred page building block |
| `inset_row` | Soft inset row capped to parent width |
| `grid` / `grid_cols` | **Grid DSL** — pinned to residual width; cols share viewport budget |
| `grid_cols_with` / `GridOpts` | Grid with striping / spacing options (page shell uses non-striped) |
| `distribute_col_max` | Pure policy: per-column max widths so sum + gaps ≤ available |
| `ColSpec` | `Flex` / `Fixed` / `MetricBps` / `MetricRate` (floors; capped by residual) |
| `metric_cell` | Right-edge mono metric; clipped to column budget |
| `RowDsl` | `heading` / `text` / `dim` / `warn` / `metric` / `metric_bps` / `metric_rate` |
| `metric_bps` / `metric_rate` | Fixed-width monospace rate strings (no staircase columns) |
| `metric_cell` / `table_metric` / `table_text` | Low-level cells (prefer the row DSL) |
| `data_table` | Index-callback table helper on top of the grid DSL |
| `reserve_system_chrome` / `system_chrome` | Android status + nav safe areas |
| `sync_system_chrome_from_android` | Measured insets from `AndroidApp::content_rect` (Android) |
| `top_header` | Header panel with system chrome already reserved |
| **App icon** | Embed a PNG as the native window icon |
| `icon_data_from_png` | Decode PNG bytes → `egui::IconData` |
| `with_app_icon` | `ViewportBuilder` + embedded PNG icon |
| `with_app_icon_id` | Same + Wayland `app_id` (match `.desktop` / `StartupWMClass`) |
| `try_with_app_icon` / `try_with_app_icon_id` | Fallible variants |

## License

MIT