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