# 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.
## 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