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

Vidya — Overview Vidya — Forms (themed checkbox) Vidya — Actions

Vidya — Palette Vidya — Typography Vidya — Surfaces

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