A Material 3 Expressive idea for developers and geeks: a universal (Android, Linux desktop) design system in Rust.
An alternative to material3-multiplatform, built from scratch in Rust as a headless core (tokens + state) with thin render adapters.
Verified baseline: Jetpack material3:1.5.0-alpha25 (stable 1.4.0) + JetBrains CMP 1.12.0 (material3:1.12.0-alpha03 = Jetpack 1.5.0-alpha22).
| Layer | Status |
|---|---|
mg-tokens: 45 color roles, 30 type styles, 10 shapes, 6 elevations, easings/durations/springs |
DONE |
mg-motion: Dynamic Motion with analytic springs + cubic-bezier + expressive/standard MotionScheme + retarget |
DONE, covered by physics tests |
mg-theme: light/dark x expressive/standard |
DONE |
mg-components: 41 of 41 components as headless Props/State |
DONE, API surface |
mg-android: winit plus wgpu shell rendering the dark expressive theme with a spring-driven indicator, packaged by cargo-apk2 |
DONE, debug and signed release APKs built by CI |
| Render adapter with real pixels (desktop + Android surface) | TODO in v0.2 |
| Dynamic-color HCT, shape morph, text layout | TODO in v0.2 |
Scope note: "all elements ported" in v0.1 means state specs for all 41 components plus exact tokens plus motion physics. Pixel rendering of each component is v0.2 work.
- Everything is inspectable: the theme serializes with
serde, a spring can be stopped at any frame to read(x, v). - Springs instead of tweens for interruptible gestures: target changes mid-flight with no jumps (
DynamicSpring::retarget). MotionScheme::expressive()vsstandard()switches in one line; spatial tracks (damping below 1.0, overshoot) are separated from effects tracks (damping exactly 1.0, strictly no overshoot).ComponentId::ALLis a registry of 41 components, guarded against drift by a test.
cargo test --workspace
cargo run -p mg-demo --bin geek_previewuse mg_motion::{DynamicSpring, MotionScheme, Tempo, Track};
use mg_tokens::motion_tokens::schemes;
let scheme = MotionScheme::expressive();
let spec = scheme.spec(Tempo::Default, Track::Spatial);
assert_eq!(spec, schemes::EXPRESSIVE_DEFAULT_SPATIAL);
let mut s = DynamicSpring::new(0.0, 1.0, spec);
s.step(1.0 / 120.0);
s.retarget(0.0);crates/mg-tokens: pure data, zero GUI dependenciescrates/mg-motion: physics with analytic solution, not Euler integrationcrates/mg-theme: scheme plus motion bindingcrates/mg-components: headless Props/State for 41 componentscrates/mg-demo: headless core demo binarycrates/mg-render: universal wgpu 2D renderer with Roboto text, shared by desktop and Androidcrates/mg-android: Android shell plus Linux desktop entry, excluded from the host workspace, built for theaarch64-linux-androidtarget in CI
Package dev.geek.material, NativeActivity, single arm64-v8a target. The shell renders a scrollable gallery of all 41 components in the dark expressive theme with Roboto text: drag to scroll, tap to toggle switches, checkboxes, radios, chips, tabs, navigation, menus and carousel pages, drag the slider, press buttons for spring morph feedback. The same scene runs on Linux desktop through the public mg_android::run_desktop entry.
CI builds two artifacts on every main push: material-geek-apk (debug, auto-generated debug key) and material-geek-apk-release (release, signed with the repo keystore and verified with apksigner verify --print-certs). Release signing keys live only in GitHub Secrets (MATERIAL_GEEK_KEYSTORE_BASE64, MATERIAL_GEEK_KEYSTORE_PASSWORD, plus reserved MATERIAL_GEEK_KEY_ALIAS and MATERIAL_GEEK_KEY_PASSWORD); no key material is committed.
On-device logs use the material-geek tag with a panic hook that forwards to logcat:
adb logcat -s material-geekInit failures (surface, adapter, device) are logged instead of panicking, the adapter request falls back from high-performance to low-power with fallback adapter, and the device is requested with the adapter's own limits rather than desktop defaults.
Android lifecycle notes: the activity is singleTask, renderer init never kills the event loop (failures retry on the next resume), and close requests are ignored on Android so the loop survives. winit allows exactly one EventLoop per process, so if the system delivers a second android_main to a reused process the shell logs it and restarts the process for a clean slate instead of dying silently.
Core workspace: single runtime dependency serde 1.0.229. No dependency on egui, iced, or dioxus anywhere. The mg-android shell crate alone uses winit 0.30, wgpu 30, pollster 1, log 0.4, and android_logger 0.15; the mg-render crate adds ab_glyph 0.2 for text, all verified as latest stable releases on crates.io. Bundled Google Sans Code Regular and Medium are SIL OFL 1.1 by the Google Sans Code Project Authors, static instances cut from the official variable font in google/fonts.
Apache-2.0. Material Design specs belong to Google; this crate is an independent Rust port, not a copy of the Compose sources.