| Bring vidya in cfd3e36 nandi 19d ago | 1 | # Vidya |
| 2 | |
| 3 | GNOME/HIG-inspired **theme layer for [egui](https://github.com/emilk/egui)** — no GTK. |
| 4 | |
| 5 | 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. |
| 6 | |
| 7 | **Repo:** [tangled.org/nandi.uk/vidya](https://tangled.org/nandi.uk/vidya) |
| 8 | |
| 9 | Screenshots below are **Waydroid** (portrait Android) captures of the demo APK. |
| 10 | |
| 11 | <p align="center"> |
| 12 | <img src="docs/screenshots/mobile/vidya-home.png" alt="Vidya — Overview" width="320" /> |
| 13 | <img src="docs/screenshots/mobile/vidya-forms.png" alt="Vidya — Forms (themed checkbox)" width="320" /> |
| 14 | <img src="docs/screenshots/mobile/vidya-actions.png" alt="Vidya — Actions" width="320" /> |
| 15 | </p> |
| 16 | |
| 17 | <p align="center"> |
| 18 | <img src="docs/screenshots/mobile/vidya-colors.png" alt="Vidya — Palette" width="320" /> |
| 19 | <img src="docs/screenshots/mobile/vidya-type.png" alt="Vidya — Typography" width="320" /> |
| 20 | <img src="docs/screenshots/mobile/vidya-surfaces.png" alt="Vidya — Surfaces" width="320" /> |
| 21 | </p> |
| 22 | |
| 23 | ## Aesthetic |
| 24 | |
| 25 | | Layer | Role | |
| 26 | |-------|------| |
| 27 | | Window / view / card / popover | Stacked surfaces (`#242424` → `#383838` in dark) | |
| 28 | | Accent | Adwaita-like blue (`#3584e4`) for primary actions, selection, and checked controls | |
| 29 | | Feedback | Destructive red, success green, warning amber | |
| 30 | | Type | Title 20 · title₂ 16 · body 14 · caption 12 | |
| 31 | | Spacing | 4 · 6 · 12 · 18 · 24 · control height 34 | |
| 32 | | Radius | 6 · 9 · 12 | |
| 33 | |
| 34 | `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. |
| 35 | |
| 36 | **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. |
| 37 | |
| 38 | 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. |
| 39 | |
| 40 | ## Demo |
| 41 | |
| 42 | 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. |
| 43 | |
| 44 | ### raylib and Jolt port |
| 45 | |
| 46 | The first native C/raylib port lives in [`raylib/`](raylib/README.md), with |
| 47 | Jolt FFI bindings and a Jolt showcase in [`jolt/`](jolt/README.md). The same ABI |
| 48 | is also implemented on **this** crate in [`ffi/`](ffi/README.md), so Jolt can |
| 49 | render through the Rust/egui layer instead of the C reimplementation — same |
| 50 | symbols, same bindings, selected by library search path. The C ABI |
| 51 | keeps raylib types private, so Jolt and Zig callers only exchange primitive |
| 52 | values and UTF-8 strings. This is an independent backend; the established Rust |
| 53 | implementation remains available while controls and layout facilities are |
| 54 | ported incrementally. |
| 55 | |
| 56 | ### glimmer |
| 57 | |
| 58 | [`glimmer/`](glimmer/README.md) is a backend for |
| 59 | [glimmer](https://github.com/jolt-lang/glimmer), Jolt's reactive toolkit — so |
| 60 | components written as hiccup, with reagent-style reactive cells, render as Vidya |
| 61 | widgets. egui has no widgets for a reconciler to patch, so the widget tree lives |
| 62 | in Rust behind a second C ABI |
| 63 | ([`ffi/include/vidya_tree.h`](ffi/include/vidya_tree.h)) that the |
| 64 | Rust/egui build alone implements: the caller mutates integer node handles, |
| 65 | one call a frame paints the whole tree, and interactions come back as events. |
| 66 | |
| 67 | ```bash |
| 68 | nix run # apps.default → vidya-demo |
| 69 | nix run .#demo |
| 70 | # or from a remote flake: |
| 71 | nix run git+https://tangled.org/nandi.uk/vidya |
| 72 | ``` |
| 73 | |
| 74 | ```bash |
| 75 | nix develop # rust (+ android target) · just · adb |
| 76 | |
| 77 | just waydroid # in-tree cargo apk → install → launch on Waydroid |
| 78 | just host # desktop egui window |
| 79 | just install # same as waydroid (rebuild APK) |
| 80 | just launch # start installed activity only |
| 81 | just shots # Waydroid screencaps → docs/screenshots/mobile/ |
| 82 | ``` |
| 83 | |
| 84 | ```bash |
| 85 | just ffi # buck2 → build/libvidya.so (host) |
| 86 | just ffi-android # buck2 → build/android/arm64-v8a/libvidya.so |
| 87 | ``` |
| 88 | |
| 89 | Both are the **same** Rust/egui sources; only the target configuration moves. |
| 90 | Android is the sole backend on device now — the earlier cimgui/raylib |
| 91 | NativeActivity port is gone. `just ffi-android` needs `ANDROID_NDK_HOME` for the |
| 92 | linker (r27+); rustc and the `aarch64-linux-android` libstd are DotSlash-pinned |
| 93 | like everything else, so there is no rustup target to add. |
| 94 | |
| 95 | `just waydroid` builds **in `android-demo/`** with the nix develop toolchain (no temp/isolated copy). |
| 96 | |
| 97 | Sections: |
| 98 | |
| 99 | - **Overview** — hero card, design tokens + **grid DSL** metrics table (demo can flip to light) |
| 100 | - **Typography** — type scale samples (grid) and hierarchy |
| 101 | - **Actions** — primary / default / destructive buttons, dialog footer, status pills |
| 102 | - **Surfaces** — layer stack table, card & header frames |
| 103 | - **Palette** — live semantic swatches (flip the shell to compare) |
| 104 | - **Forms** — themed inputs, accent checkbox, slider, combo, progress |
| 105 | |
| 106 | ### Refreshing screenshots |
| 107 | |
| 108 | With a running Waydroid session: |
| 109 | |
| 110 | ```bash |
| 111 | just install # rebuild + install APK |
| 112 | just shots # adb screencap per section → docs/screenshots/mobile/ |
| 113 | ``` |
| 114 | |
| 115 | ## Use |
| 116 | |
| 117 | Apps should start dark-only: |
| 118 | |
| 119 | ```toml |
| 120 | [dependencies] |
| 121 | vidya = { git = "https://tangled.org/nandi.uk/vidya" } |
| 122 | # or: vidya = { git = "ssh://git@tangled.org/nandi.uk/vidya" } |
| 123 | egui = "0.31" |
| 124 | ``` |
| 125 | |
| 126 | ```rust |
| 127 | use vidya::{apply_dark, checkbox, primary_button, Theme}; |
| 128 | |
| 129 | apply_dark(ctx); |
| 130 | let th = Theme::dark(); |
| 131 | if primary_button(ui, &th, "Open").clicked() { |
| 132 | // … |
| 133 | } |
| 134 | checkbox(ui, &th, &mut sync, "Sync preferences"); |
| 135 | ``` |
| 136 | |
| 137 | `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). |
| 138 | |
| 139 | ### Window icon |
| 140 | |
| 141 | Embed a PNG and attach it to the viewport (set Wayland `app_id` to match your `.desktop`): |
| 142 | |
| 143 | ```rust |
| 144 | use egui::ViewportBuilder; |
| 145 | use vidya::with_app_icon_id; |
| 146 | |
| 147 | let viewport = with_app_icon_id( |
| 148 | ViewportBuilder::default().with_title("I/O Usage"), |
| 149 | "usage", |
| 150 | include_bytes!("../assets/usage-256.png"), |
| 151 | ); |
| 152 | ``` |
| 153 | |
| 154 | ## Nix flake |
| 155 | |
| 156 | | Output | Role | |
| 157 | |--------|------| |
| 158 | | `apps.default` / `apps.demo` | Desktop showcase via **`cargo run`** (rustup + egui libs) | |
| 159 | | `packages.default` / `packages.demo` | Same launcher + `.desktop` entry | |
| 160 | | `packages.vidya` | Theme library sources + rlib bundle | |
| 161 | | `devShells.default` | just · adb · egui runtime libs (rustup for cargo) | |
| 162 | |
| 163 | ```bash |
| 164 | nix run # cargo run --manifest-path host/Cargo.toml |
| 165 | nix run .#demo |
| 166 | nix build .#vidya |
| 167 | nix develop |
| 168 | ``` |
| 169 | |
| 170 | `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/`. |
| 171 | |
| 172 | As a flake input: |
| 173 | |
| 174 | ```nix |
| 175 | inputs.vidya.url = "git+https://tangled.org/nandi.uk/vidya"; |
| 176 | ``` |
| 177 | |
| 178 | ## API |
| 179 | |
| 180 | | Item | Role | |
| 181 | |------|------| |
| 182 | | `Theme::dark()` / `light()` | Palette + spacing + type scale (`dark` for apps; `light` optional) | |
| 183 | | `apply` / `apply_dark` / `apply_light` | Install on `egui::Context` (+ symbol font); prefer `apply_dark` | |
| 184 | | `install_symbol_font` | Fallback glyphs for arrows / disclosure triangles / bullets / quotes | |
| 185 | | `emoji_icon` / `paint_emoji_in` / `has_emoji_icon` | Full Twemoji color set by codepoint | |
| 186 | | `Icon` / `icon` / `paint_icon_in` | Named shortcuts + stroke Plus / Copy | |
| 187 | | `icon_button` | Square tool button with a stroke/emoji icon | |
| 188 | | `reaction_chip` | Themed count chip with color emoji | |
| 189 | | `primary_button` / `button` / `destructive_button` | Styled actions | |
| 190 | | `checkbox` | Accent-filled checkbox with drawn checkmark | |
| 191 | | `status_dot` | Live/offline circle (drawn — no Unicode tofu on Android) | |
| 192 | | `text_field_singleline` / `text_field_multiline` | Text inputs with field padding (fill parent width) | |
| 193 | | `consume_command` / `consume_escape` | Consume Cmd/Ctrl+key or Esc (platform-aware) | |
| 194 | | `command_shortcut_label` / `escape_label` | `"Ctrl+F"` / `"⌘F"` / `"Esc"` for tooltips | |
| 195 | | `title` / `title_2` / `body` / `dim_label` | Text roles | |
| 196 | | `Theme::header_frame` / `card_frame` / `page_frame` | Layout chrome | |
| 197 | | `dialog` | Centered resizable window with `card_frame` (chain size / `.show`) | |
| 198 | | `Theme::text_edit_margin` | Inner field padding (12×8 default) | |
| 199 | | **Layout composition** | Prefer these over raw `set_max_width` / `Layout` plumbing | |
| 200 | | `fit_width` / `fill_width` | Pin children to residual width (no edge overflow) | |
| 201 | | `vstack` | Non-justified vertical stack (no giant gaps in tall parents) | |
| 202 | | `card` | Fill-width themed card + `vstack` content | |
| 203 | | `compact_card` | Fixed-width card that **hugs content** (gauge / anomaly tiles) | |
| 204 | | `pack` | Wrap compact tiles without stretching leftover horizontal space | |
| 205 | | `hflow` | Wrapping horizontal row (toolbars / chips) | |
| 206 | | `lead_trail` | Leading field + trailing actions without clipping the actions | |
| 207 | | `two_col` / `side_by_side` | Responsive two-column / stack (+ pure breakpoint policy) | |
| 208 | | `page_body` / `central_page` | **Enforced** scrollable page: top-level content is a **grid** (`GridCtx`) | |
| 209 | | `page_body_cols` / `central_page_cols` | Same, with explicit top-level `ColSpec`s | |
| 210 | | `page_scroll` | Escape hatch: scroll + width pin only (no grid) | |
| 211 | | `GridCtx::section` | Full-width page section (one row / one cell) — preferred page building block | |
| 212 | | `inset_row` | Soft inset row capped to parent width | |
| 213 | | `grid` / `grid_cols` | **Grid DSL** — pinned to residual width; cols share viewport budget | |
| 214 | | `grid_cols_with` / `GridOpts` | Grid with striping / spacing options (page shell uses non-striped) | |
| 215 | | `distribute_col_max` | Pure policy: per-column max widths so sum + gaps ≤ available | |
| 216 | | `ColSpec` | `Flex` / `Fixed` / `MetricBps` / `MetricRate` (floors; capped by residual) | |
| 217 | | `metric_cell` | Right-edge mono metric; clipped to column budget | |
| 218 | | `RowDsl` | `heading` / `text` / `dim` / `warn` / `metric` / `metric_bps` / `metric_rate` | |
| 219 | | `metric_bps` / `metric_rate` | Fixed-width monospace rate strings (no staircase columns) | |
| 220 | | `metric_cell` / `table_metric` / `table_text` | Low-level cells (prefer the row DSL) | |
| 221 | | `data_table` | Index-callback table helper on top of the grid DSL | |
| 222 | | `reserve_system_chrome` / `system_chrome` | Android status + nav safe areas | |
| 223 | | `sync_system_chrome_from_android` | Measured insets from `AndroidApp::content_rect` (Android) | |
| 224 | | `top_header` | Header panel with system chrome already reserved | |
| 225 | | **App icon** | Embed a PNG as the native window icon | |
| 226 | | `icon_data_from_png` | Decode PNG bytes → `egui::IconData` | |
| 227 | | `with_app_icon` | `ViewportBuilder` + embedded PNG icon | |
| 228 | | `with_app_icon_id` | Same + Wayland `app_id` (match `.desktop` / `StartupWMClass`) | |
| 229 | | `try_with_app_icon` / `try_with_app_icon_id` | Fallible variants | |
| 230 | |
| 231 | ## License |
| 232 | |
| 233 | MIT |