A developer and AI reference for the Rustinaz/code-template workspace.
This document is the companion to README.md. The README explains what the template is and how to get it running; this guide explains how it is built and how to write code inside it — the crate-by-crate contract, the invariants a change must not break, and a set of task recipes.
It is written so that a person and an AI coding assistant can both work from it without guessing: every path, crate and public API it mentions exists in this repository.
- 🇮🇷 نسخهٔ فارسی این راهنما: TEMPLATE.fa.md
- 🇬🇧 Getting started (English): README.md
- 🇮🇷 شروع سریع (فارسی): README.fa.md
Author: Sina Khanzadeh · khanzadeh.1377@gmail.com · sina-khanzadeh.ir Instagram / Telegram / Discord: programmer_1998 Licence: MIT OR Apache-2.0
- How to use this document
- What the template is
- Architecture at a glance
- The complete file tree
- The crates, one by one
- The UI layer in detail
- The apps
- The wire contract and the bundled backend
- Feature flags and the
cfgmatrix - Commands: build, run, test, lint
- Continuous integration
- Recipes: how to add things
- Invariants a change must not break
- Troubleshooting and gotchas
- Notes for AI coding assistants
- References and contact
If you are a human developer: read §2–§3 to load the mental model, skim §5 as a map, then use §12 (recipes) whenever you add something. §14 collects the traps that are easy to hit.
If you are an AI assistant: before writing any code,
- read §3 (the dependency graph — it dictates where new code belongs),
- read §5 (which crate owns which responsibility),
- pick the matching recipe in §12,
- obey §13 (invariants) and §9 (feature flags /
cfg), - run the commands in §10 to prove the change.
Do not invent crates, modules or APIs. Everything that exists is listed in §4 and §5. If something is not there, the recipe in §12 tells you where it belongs.
Rustinaz is a Rust-first cross-platform application template. One workspace produces:
| Target | Artifact | Where the entry point lives |
|---|---|---|
| Linux / Windows / macOS | a native desktop binary | apps/example/src/main.rs → example_app::run() |
| Android | a .so loaded by android.app.NativeActivity |
apps/example/src/lib.rs → android_main() |
| iOS | a static library linked into an Xcode app | apps/example/src/lib.rs (the staticlib crate type) |
| Web | a .wasm + index.html bundled by Trunk |
apps/example/src/lib.rs → run_web() |
| (any) | the bundled JSON backend | apps/server/src/main.rs |
The bet the template makes is simple: write the UI once in egui and ship the same widget tree to every target. Only what genuinely differs per operating system — window creation, theme defaults, device information, accessibility announcements, JNI — lives in per-platform code.
There is no Java, Kotlin, Swift, or HTML/CSS application code. The Android project
(android-app/) contains only Gradle build scripts (Kotlin DSL) and XML resources (manifest,
theme, icon) — the APK ships zero .class files and the Rust .so draws every pixel.
These are the real edges, taken from cargo metadata. The direction is strictly one-way;
nothing below ever depends on something above it.
┌──────────────┐
│ shared │ domain, config, errors, api, services (no UI, no OS)
└──────┬───────┘
┌───────────────┼────────────────┬─────────────────┐
│ │ │ │
┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼───────┐ ┌──────▼────────┐
│ ui │ │ resources │ │ build-config │ │ platform/* │
│ (egui, │ │ (strings, │ │ (version, │ │ (linux, …) │
│ tokens, │ │ colours) │ │ signing) │ │ │
│ components)│ └──────┬──────┘ └──────────────┘ └──────┬────────┘
└──────┬──────┘ │ │
│ │ ui is also a dependency ─┘
│ │ of every platform/* crate
└───────────────┴───────────────┬─────────────────┘
│
┌─────────▼──────────┐ ┌──────────┐
│ apps/example │ │apps/server│
│ (the application) │ │ (backend) │
└────────────────────┘ └─────┬────┘
│
shared only
Read aloud:
shareddepends on nothing internal. Domain types, config, errors, the wire contract (api) and the service traits + in-memory implementations.uidepends onshared. Theme tokens, layout maths, navigation, the component library, and thePlatformUi/Windowtraits.platform/*depend onsharedandui. They add the OS glue on top ofui's traits.resourcesandbuild-configdepend onshared.apps/exampledepends onshared,ui,resourcesand all sixplatform/*crates (each gated by#[cfg(target_os = …)], see §7.1).apps/serverdepends onsharedonly — the backend never pullsuior a platform crate into its binary, so it carries no GUI code.
Two things are called "platform".
uiships its ownPlatformUiimplementations undercrates/ui/src/platform/, while the OS-facing crates live incrates/platform/*. Only the latter are separate workspace members, and they depend onui— never the reverse.
Desktop and Android both funnel through the same boot sequence in apps/example/src/lib.rs:
main() / android_main() / run_web()
│
▼
bootstrap(&CliOptions)
│ 1. AppConfig::load() (or --config PATH)
│ 2. apply --platform / --server overrides
│ 3. shared::init_blocking(config) → tracing + globals
│ 4. platform_*::init() → OS setup (per target)
│ 5. ResourceManager::init_global(config) → strings/colours/themes
│ 6. UiBuilder::new(config)
│ .platform(platform) → corrects host → device
│ .framework(UiFramework::Egui)
│ .build() → UiContext
▼
eframe::run_native(...) (desktop) | WebRunner::start(...) (web)
│
▼
ExampleApp::update(&egui::Context) → draws with egui, reads UiContext
UiContext (in crates/ui) is the object the UI reads from: the shared AppConfig, the
detected DeviceInfo, the resolved ThemeTokens, and the active UiFramework.
Everything below exists in the repository (generated output such as target/,
apps/example/dist/ and android-app/build/ is omitted).
code-template/
├── Cargo.toml workspace: 12 members, workspace dependency versions, profiles
├── Cargo.lock committed on purpose: reproducible first build
├── rust-toolchain.toml pins stable + rustfmt + clippy (editors and CI agree)
├── .editorconfig UTF-8, LF, 4-space (2 for TOML/YAML)
├── .gitignore
├── .cargo/
│ └── config.toml net settings + the short cargo aliases (no hard-coded linkers)
├── .github/
│ ├── dependabot.yml
│ └── workflows/ci.yml the six CI gates
├── .vscode/ committed run config, tasks, rust-analyzer settings, extensions
│
├── apps/
│ ├── example/ ← the app you actually edit
│ │ ├── Cargo.toml crate-type = rlib, cdylib, staticlib; per-target deps
│ │ ├── Trunk.toml web build config
│ │ ├── index.html the <canvas> eframe mounts into (id = the_canvas_id)
│ │ └── src/
│ │ ├── lib.rs run(), run_web(), android_main(), bootstrap(), the UI
│ │ ├── main.rs thin desktop binary → example_app::run()
│ │ └── backend.rs the client's Backend panel (native) / placeholder (web)
│ └── server/ the bundled backend
│ ├── Cargo.toml
│ └── src/
│ ├── lib.rs routing: endpoint constants → handlers; serve()
│ ├── http.rs hand-written HTTP/1.1 parse + response write
│ └── main.rs clap binary (--bind, --no-demo), demo seeding
│
├── crates/
│ ├── shared/ business logic (no UI, no OS)
│ │ ├── Cargo.toml the network feature lives here
│ │ └── src/
│ │ ├── lib.rs init()/init_blocking(), re-exports, DefaultNetworkService alias
│ │ ├── domain/mod.rs User, AppSettings, DeviceInfo, Platform, ThemeTokens, Route, …
│ │ ├── config/mod.rs AppConfig, ConfigBuilder, ThemeMode, FeatureFlags
│ │ ├── errors/mod.rs AppError + sub-errors, ErrorContext
│ │ ├── api/mod.rs the wire contract: endpoints + request/response types
│ │ └── services/
│ │ ├── mod.rs service traits, in-memory impls, ServiceContainer, factory
│ │ └── http.rs HttpNetworkService: std-only blocking HTTP/1.1 client
│ ├── ui/ shared UI (this is where the widget library lives)
│ │ ├── Cargo.toml the egui feature lives here (default on)
│ │ └── src/
│ │ ├── lib.rs UiContext, UiBuilder, UiFramework, responsive helpers
│ │ ├── theme/mod.rs ThemeManager, ThemeBuilder, built-in themes, CSS export
│ │ ├── layout/mod.rs LayoutCalculator, FlexLayout, GridLayout, builders
│ │ ├── navigation/mod.rs NavigationController, deep links, bottom-nav/drawer configs
│ │ ├── components/ the component library
│ │ │ ├── mod.rs Component trait, ComponentProps, colour helpers, re-exports
│ │ │ ├── app_bar.rs AppBar (+ BACK_ACTION)
│ │ │ ├── button.rs Button, ButtonStyle, MIN_TOUCH_TARGET
│ │ │ ├── card.rs Card
│ │ │ ├── dialog.rs Dialog, DialogState
│ │ │ ├── navigation_bar.rs NavItem, NavigationBar
│ │ │ ├── scaffold.rs Scaffold, ScaffoldLayout, Slot
│ │ │ └── text_field.rs TextField, TextFieldState
│ │ ├── platform/
│ │ │ ├── mod.rs PlatformUi + Window traits, PlatformTheme, PlatformFactory,
│ │ │ │ keyboard/pointer/text-input behaviours
│ │ │ ├── linux.rs LinuxUi / LinuxWindow (in-crate PlatformUi impls)
│ │ │ ├── windows.rs WindowsUi / WindowsWindow
│ │ │ ├── macos.rs MacosUi / MacosWindow
│ │ │ ├── android.rs AndroidUi / AndroidWindow
│ │ │ ├── ios.rs IosUi / IosWindow
│ │ │ └── web.rs WebUi / WebWindow
│ │ ├── hooks/mod.rs present but NOT declared (not part of the egui stack yet)
│ │ └── utils/mod.rs present but NOT declared (not part of the egui stack yet)
│ ├── resources/ strings, colours, themes, assets (runtime loaded)
│ │ ├── Cargo.toml
│ │ └── src/lib.rs ResourceManager, ResourceBuilder, StringMap, ColorMap
│ ├── build-config/ version / target / signing metadata
│ │ ├── Cargo.toml
│ │ └── src/lib.rs BuildTimeConfig, VersionInfo, platform_build_config, signing
│ └── platform/ the OS glue (one crate per target)
│ ├── linux/ src/lib.rs platform_info(), init()
│ ├── windows/ src/lib.rs init(), window/theme helpers
│ ├── macos/ src/lib.rs init(), window/theme helpers
│ ├── android/ src/lib.rs AndroidApp, install_app/current_app, device_info(), JNI, init()
│ ├── ios/ src/lib.rs IosInfo, device_info(), init(), haptics
│ └── web/ src/lib.rs user agent, viewport, touch detection
│
├── android-app/ Android Studio packaging project (no app source)
│ ├── settings.gradle.kts
│ ├── build.gradle.kts
│ ├── gradle.properties
│ ├── gradlew / gradlew.bat
│ ├── gradle/wrapper/… wrapper jar + properties
│ └── app/
│ ├── build.gradle.kts runs Cargo per ABI, then packages the .so
│ ├── proguard-rules.pro
│ └── src/main/
│ ├── AndroidManifest.xml points at android.app.NativeActivity
│ └── res/… strings, theme, launcher icon
│
├── resources/ shared data files
│ ├── colors/colors.json
│ └── strings/strings_en.json · strings_es.json
│
└── scripts/ one script per platform, plus the dev loop
├── android-env.sh exports NDK linkers/CC for Cargo (source it)
├── build-all.sh build every target this machine can
├── build-linux.sh build-windows.sh build-macos.sh build-ios.sh
├── build-android.sh build-web.sh
└── dev.sh start the backend, wait for the port, run the client
The business-logic crate. No egui, no OS types. Re-exported from the crate root
(crates/shared/src/lib.rs) so downstream code can use shared::….
| Module | Owns | Key public items |
|---|---|---|
shared::domain |
Core data model | EntityId (= Uuid), Timestamp, Entity/RepositoryEntity traits, User, UserPreferences, AppSettings, AccessibilitySettings, WindowState, WindowPosition, DeviceInfo, Platform, FormFactor, ScreenSize, Orientation, SafeArea, Route, ThemePreference, ThemeTokens, ColorTokens/ColorRole/ColorValue, TypographyTokens, SpacingTokens, ShapeTokens, MotionTokens, ElevationTokens, Shadow, BuildConfig, SigningConfig |
shared::config |
Runtime configuration | AppConfig, ConfigBuilder, AppInfo, PlatformConfig, UiConfig, ThemeConfig, ThemeMode, ResponsiveBreakpoints, NetworkConfig, StorageConfig, LoggingConfig, LogLevel/LogFormat/LogOutput, FeatureFlags. AppConfig::load(), ::save(), ::device_info(), ::theme_tokens(), ::is_feature_enabled() |
shared::errors |
The single error type | AppError (wraps ConfigError, NetworkError, StorageError, AuthError, ValidationError, PlatformError, UiError, SerializationError, plus Io, Http, Json, NotFound, Timeout, Unsupported, Multiple), Result<T>, ErrorContext/ErrorContextExt |
shared::api |
The wire contract | endpoints::{HEALTH, USERS, USERS_SEARCH}, HealthResponse, ErrorResponse, RegisterRequest, UserList, … |
shared::services |
Service traits + in-memory impls | traits AuthService, UserService, SettingsService, NetworkService, StorageService, PlatformService, AnalyticsService, NotificationService; InMemoryRepository<T>; ServiceContainer, AppServices, PlatformServices; services::defaults::*; services::factory::create_services(); HttpNetworkService |
Initialisation. shared::init_blocking(config) -> Result<Arc<AppConfig>> loads config,
installs tracing, and returns the shared config. shared::init() is the same, async, for
callers inside a runtime. Use init_blocking from main, android_main or a extern "C"
entry point.
The network aliasing trick. shared re-exports one name, DefaultNetworkService, which
is either the reqwest-backed client (feature network) or StubNetworkService (default).
Calling code is identical either way and simply gets AppError::Unsupported at runtime when the
stub is compiled in. HttpNetworkService is separate and always available off-web.
The shared UI crate. Two halves, split by the egui feature (default on):
- Pure logic (always compiles): tokens, layout maths, builders, component state and sizing.
- Rendering (
#[cfg(feature = "egui")]): theshow(&mut egui::Ui, …)methods.
| Module | Key public items |
|---|---|
ui (root) |
UiFramework, UiContext, UiBuilder, module ui::responsive |
ui::theme |
ThemeManager, ThemeBuilder, ThemeChangeListener; built-in themes material3(), ios(), material_you(), high_contrast(), minimal(); generate_css() |
ui::layout |
LayoutConstraints, LayoutCalculator, FlexLayout, GridLayout, LayoutBuilder, FlexLayoutBuilder, GridLayoutBuilder, LayoutDirection, FlexDirection, JustifyContent, AlignItems, AlignContent, FlexWrap, GridAutoFlow |
ui::navigation |
NavigationController, NavEntry, NavDirection, DeepLinkHandler, NavigationBuilder, NavigationListener, BottomNavConfig/BottomNavItem, NavigationDrawerConfig/DrawerHeader/DrawerItem/DrawerFooter |
ui::components |
see §5.2.1 |
ui::platform |
PlatformUi, Window, WindowConfig, PlatformTheme, PlatformColors, PlatformFonts, PlatformSpacing, PlatformShapes, PlatformMotion, PlatformFactory, and the keyboard/pointer/text_input behaviour helpers |
Every component follows the same two-part shape: pure logic (builders, state, sizing) that
compiles with egui off, and a show(…) method gated behind #[cfg(feature = "egui")].
| Component | Constructed with | Renders with |
|---|---|---|
AppBar |
AppBar::new(title) + .subtitle(), .action(), .back_enabled(), .center_title(), .elevated() |
show(ui, …) (feature egui) |
Button |
Button::new(label) + .style(ButtonStyle), .size(ComponentSize), .enabled(), .icon(char), .full_width() |
returns a ButtonResponse |
Card |
Card::new(title) + .subtitle(), .elevation(f32), .padding_multiplier(), .clickable() |
show(ui, ctx) -> ButtonResponse / show_with(…) |
Dialog |
Dialog::new(title) + .body(), .dismissable(), .visible(), .add_action() |
show(…), driven by a DialogState |
NavigationBar |
NavigationBar::new(Vec<NavItem>) + .items(), .select(), .select_next(), .show_labels() |
show(…) |
Scaffold |
ScaffoldLayout::new(app_bar, navigation_bar, body) of Slots |
layout maths; ScaffoldResponse |
TextField |
TextField::new(); state via TextFieldState::new(placeholder) + .with_max_length(), .with_password(), .with_disabled(), .set_value(), .take_changed() |
show(…) |
Also public: Component trait, ComponentProps, ComponentVariant, ComponentSize,
ComponentState, AccessibilityRole, ButtonResponse, MIN_TOUCH_TARGET, BACK_ACTION, and
the colour helpers role_value, value, role, token, with_alpha, state_fill.
One crate per target. Each depends on shared and ui and exposes a small, imperative API plus
a PlatformUi implementation where the OS has something to say (window, theme, accessibility).
| Crate | Public surface | Used from |
|---|---|---|
platform-linux |
platform_info(), PlatformInfo, init() |
apps/example init_platform() |
platform-windows |
init(), window/theme helpers |
apps/example |
platform-macos |
init(), window/theme helpers |
apps/example |
platform-android |
AndroidApp, install_app(), current_app(), device_info(), AndroidInfo, init(), material3_theme(), AndroidPlatformUi, AndroidWindow |
apps/example android_main() |
platform-ios |
IosInfo, device_info(), init(), haptics |
apps/example |
platform-web |
user-agent, viewport and touch detection | apps/example (via the web target) |
The
uicrate also contains per-platformPlatformUiimplementations undercrates/ui/src/platform/*.rs. The distinction is deliberate:ui::platformdefines the abstractions and default impls; thecrates/platform/*crates do the real OS work (JNI, native windows,uname,Win32API,web-sys). When you need the OS, editcrates/platform/*; when you need the shape/contract, editcrates/ui/src/platform/mod.rs.
Runtime resource loading: strings, colours, themes, assets and fonts, loaded from the
resources/ directory and cached process-wide.
ResourceManager::init_global(config) -> Arc<ResourceManager>,::global(),::global_or_init()load_all(),reload(),add_resource_dir(path)- Getters:
get_string(key, locale),get_string_formatted(…),get_color(name),get_theme(name),current_theme(),get_asset(name),get_asset_data(name),get_font(name),get_raw(name) ResourceBuilder+StringMap/ColorMapfor building resources in code
Data files: resources/strings/strings_en.json, resources/strings/strings_es.json,
resources/colors/colors.json.
Version, target and signing metadata.
generate_build_metadata() -> BuildTimeConfig, withVersionInfo,GitInfo,RustcInfo,TargetInfoplatform_build_config(platform) -> BuildConfig- Signing:
load_signing_config,save_signing_config,android_signing_config,ios_signing_config BuildTarget::all_targets(),target_for_platform(platform)- A small
clapCli/Commandsfor printing build info
UiContext is what the drawing code reads; UiBuilder is what produces it during boot.
// crates/ui/src/lib.rs
pub struct UiContext {
pub config: Arc<RwLock<AppConfig>>, // parking_lot::RwLock
pub device_info: DeviceInfo,
pub theme_tokens: ThemeTokens,
pub framework: UiFramework,
}
impl UiContext {
pub fn platform(&self) -> Platform;
pub fn is_mobile(&self) -> bool;
pub fn is_desktop(&self) -> bool;
pub fn theme_preference(&self) -> ThemePreference;
pub fn color_tokens(&self) -> &ColorTokens;
pub fn spacing(&self) -> &SpacingTokens;
pub fn typography(&self) -> &TypographyTokens;
}let ui_context = UiBuilder::new(config) // Arc<RwLock<AppConfig>>
.platform(Platform::Android) // device, not the build host
.framework(UiFramework::Egui) // egui is the only backend today
.build();.platform(...) matters on Android, where the config file records the build host's platform.
.device_info(...) overrides the detected DeviceInfo entirely.
ThemeManager holds a ThemeTokens and notifies listeners on change. ThemeBuilder produces
tokens; the built-in themes are material3(), ios(), material_you(), high_contrast() and
minimal(). generate_css(&tokens) renders CSS for a web surface.
LayoutCalculator::new(device_info) answers the responsive questions the widgets ask:
grid_columns(base), spacing(base), font_scale(base), is_compact(), is_two_pane(),
safe_area(), breakpoint(), is_landscape() / is_portrait().
The builders LayoutBuilder, FlexLayoutBuilder, GridLayoutBuilder describe flex and grid
layouts declaratively, and ui::responsive offers free helpers (grid_columns,
spacing_multiplier, font_scale, is_compact, is_two_pane).
NavigationController keeps a route stack (navigate, push, pop, replace, reset,
current, previous, stack, can_go_back, depth, clear) and notifies
NavigationListeners. DeepLinkHandler maps URL patterns to Routes.
NavigationBuilder::new(initial_route).route(name, route).deep_link(pattern, resolver).build().
// crates/ui/src/platform/mod.rs
pub trait PlatformUi: Send + Sync {
fn platform(&self) -> Platform;
fn create_window(&self, config: WindowConfig) -> Result<Box<dyn Window>>;
fn theme(&self) -> PlatformTheme;
fn announce_for_accessibility(&self, text: &str);
// …plus the behaviour helpers below
}
pub struct PlatformFactory;
impl PlatformFactory {
pub fn create(platform: Platform) -> Box<dyn PlatformUi> { /* … */ }
}PlatformTheme groups PlatformColors, PlatformFonts, PlatformSpacing, PlatformShapes
and PlatformMotion. The keyboard, pointer and text_input submodules translate the
platform's conventions (platform_key_mapping, platform_pointer_behavior,
platform_text_input_behavior, and the KeyCode/PointerType/TextInputConfig types).
The whole application lives in the library (src/lib.rs), with a thin binary
(src/main.rs) so that the same code becomes a desktop binary, an Android .so, a browser
.wasm and an iOS .a. The crate types are declared in Cargo.toml:
crate-type = ["rlib", "cdylib", "staticlib"], library name example_app.
Entry points
| Entry point | Target | Notes |
|---|---|---|
run() |
desktop | #[cfg(not(target_arch = "wasm32"))]; parses CLI, boots, run_egui |
run_web() |
web | #[wasm_bindgen(start)], async; finds <canvas id="the_canvas_id"> |
android_main(app) |
Android | #[no_mangle] extern "C"; install_app(app) then boots and runs |
CLI (build_cli() / parse_args() → CliOptions): --platform, --framework, --debug
(-d), --config (-c PATH), --server URL. --server overrides network.base_url for a
single run; the built-in default already points at the bundled backend.
Platform deps are cfg-gated in Cargo.toml, so a desktop build never compiles the JNI or
web-sys code:
[target.'cfg(target_os = "android")'.dependencies]
winit = { workspace = true }
android-activity = { workspace = true }
platform-android = { path = "../../crates/platform/android" }
[target.'cfg(target_os = "linux")'.dependencies]
platform-linux = { path = "../../crates/platform/linux" }
# …and windows / macos / ios / wasm32The browser has no clock. wasm32-unknown-unknown aborts on Instant::now() and
SystemTime::now(). Anything that needs time uses web_time, which is std::time on native
targets and performance.now()/Date.now() in the browser.
The backend panel (src/backend.rs) calls the bundled server without blocking the UI:
requests run on a worker thread and post results back over an mpsc channel. On the web — where
there are no raw sockets — it renders a placeholder instead of pretending to connect.
A library + binary: the routing/HTTP logic is testable without binding a socket
(server::Server, server::serve), and main.rs is the clap binary.
cargo run --package server # 127.0.0.1:8080, demo users seeded
cargo run --package server -- --bind 0.0.0.0:8080 # reachable from an emulator / LAN
cargo run --package server -- --no-demo # empty user storePublic surface: Server::new(services), Server::handle(&Request) -> Response,
Server::services(), serve(listener, server), DEFAULT_LIMIT = 50, MAX_LIMIT = 200. The
HTTP/1.1 wire code is hand-written (src/http.rs); the seam for a framework such as axum is
the routing table in src/lib.rs. The binary seeds two demo users
(alice@example.com, bob@example.com, password password123) unless --no-demo is given.
The contract is written once in crates/shared/src/api/mod.rs and imported by both ends,
so the client and server cannot drift:
pub mod endpoints {
pub const HEALTH: &str = "/api/health";
pub const USERS: &str = "/api/users";
pub const USERS_SEARCH: &str = "/api/users/search";
}- Server side:
apps/server/src/lib.rsmaps theendpointsconstants to handlers. - Client side:
shared::services::HttpNetworkService— a blocking HTTP/1.1 client built onstd::net,#[cfg(not(target_arch = "wasm32"))], speaking plainhttp://. It does not usereqwest, so it needs neither thenetworkfeature nor a TLS stack, and the Android / web builds stay TLS-free.
let client = HttpNetworkService::new("http://127.0.0.1:8080", Duration::from_millis(5_000));
let health = client.get_json(endpoints::HEALTH)?;
let created = client.post_json(endpoints::USERS, &serde_json::to_value(&request)?)?;Run both halves together with scripts/dev.sh (it starts the backend, waits until the port
answers, runs the client pointed at it with --server, and stops the backend when the client
exits). On Android, forward the host port first: adb reverse tcp:8080 tcp:8080.
| Feature | Crate | Default | Effect |
|---|---|---|---|
network |
shared |
off | Enables the reqwest (TLS) network client. Off ⇒ StubNetworkService, which returns AppError::Unsupported and cross-compiles everywhere with no C toolchain. |
egui |
ui |
on | Turns shared component logic into real widgets. Off ⇒ the crate still builds (platform crates need its tokens/traits), but show(…) methods disappear. |
hot_reload |
resources |
off | Reserved for hot-reloading resources. |
Verify honestly:
cargo check -p shared # stub network, no TLS
cargo build -p shared --features network # real reqwest client
cargo check -p ui --no-default-features # components without a renderercfg |
Applies to | What it gates |
|---|---|---|
target_os = "android" |
Android | android_main, JNI, platform-android, winit android glue |
target_os = "linux" / "windows" / "macos" / "ios" |
that desktop/mobile OS | the matching platform-* crate and init() arm |
target_arch = "wasm32" |
web | run_web, WEB_CANVAS_ID, web-sys, getrandom/js, uuid/js |
not(target_arch = "wasm32") |
everything else | run(), native_options(), HttpNetworkService |
feature = "egui" |
ui |
every component show(…) |
feature = "network" |
shared |
the reqwest client |
Rule of thumb: entry-point code that cannot exist on a target must be #[cfg]-gated, not
if-ed at runtime. The template already models this for run/run_web/android_main.
cargo check --workspace --all-targets # 1. everything, every crate and target
cargo test --workspace --all-targets # 2. the tests
cargo clippy --workspace --all-targets --all-features -- -D warnings # 3. lints = errorsPlus cargo fmt --all. CI runs the same, and also cargo doc --workspace --no-deps with
RUSTDOCFLAGS=-D warnings.
Defined in .cargo/config.toml. None is named after a real Cargo subcommand on purpose (an
alias of the same name resolves to itself and recurses forever).
| Alias | Expands to |
|---|---|
cargo c |
check --workspace --all-targets |
cargo t |
test --workspace --all-targets |
cargo b / cargo br |
build --workspace / --release |
cargo f |
fmt --all |
cargo l |
clippy --workspace --all-targets |
cargo check-all |
check --workspace --all-targets --all-features |
cargo server |
run --package server |
cargo deps |
tree --workspace --depth 2 |
cargo docs |
doc --workspace --no-deps |
cargo run --package example-app # desktop, default backend URL
cargo run --package example-app -- --debug # -d for debug logging
cargo run --package example-app -- --server http://192.168.1.20:8080
scripts/build-all.sh --list # what this machine can build
scripts/build-all.sh # build all of them
scripts/build-all.sh android web # just these
scripts/build-linux.sh [--release]
scripts/build-android.sh [--release] [--abi arm64-v8a] [--apk]
scripts/build-windows.sh [--release] # MinGW-w64, or run on Windows
scripts/build-macos.sh [--release] # macOS only
scripts/build-ios.sh [--simulator] # macOS only
scripts/build-web.sh [--serve] # needs trunk
scripts/dev.sh [--release] [--bind ADDR] [--server-only|--client-only]Android without Gradle (just the .so):
source scripts/android-env.sh
cargo build --package example-app --target aarch64-linux-android --release
# -> target/aarch64-linux-android/release/libexample_app.so.github/workflows/ci.yml defines the gates. A change is "done" only when all of them pass.
| Job | Runner | Proves |
|---|---|---|
checks |
ubuntu | cargo fmt --check, clippy … -D warnings, check --all-features, test --all-features, doc -D warnings |
backend |
ubuntu | builds server, starts it, and curls /api/health, /api/users, /api/users/search?q=… |
cross-compile |
ubuntu (matrix) | Android (4 ABIs) produce libexample_app.so; Web builds via Trunk |
android-apk |
ubuntu | JDK 17 + NDK + Gradle assemble a real debug .apk |
desktop |
ubuntu / macos / windows (matrix) | the native binary builds on each OS |
ios |
macos | the iOS static library compiles (signing is out of scope for CI) |
Honest scope: CI proves compilation, linting and tests on every target; it does not prove a signed store build, and it does not run the app on a physical device. See the README's "What is verified, and what is not" section for the project's own honesty statement.
Each recipe tells you which file to touch and which invariants to respect (§13).
- Add a variant to
Routeincrates/shared/src/domain/mod.rs. - Navigate to it with a
NavigationController(ui::navigation) —navigate(route, NavDirection::Forward). - Render it inside
ExampleApp::update(or your owneframe::App) by matching oncontroller.current()and calling adraw_*helper. - Keep the drawing code in the app (
apps/example) — the route model belongs inshared.
- Create
crates/ui/src/components/<name>.rs. - Pure logic first: the struct, a
new(...), builder methods, and any sizing/state maths — all with noeguitypes, so it compiles with theeguifeature off. - Add the rendering method behind
#[cfg(feature = "egui")] pub fn show(&self, ui: &mut egui::Ui, ctx: &UiContext). - Re-export it from
crates/ui/src/components/mod.rs(pub use <name>::…;). - Add a unit test for the logic that runs without
egui.
pub struct Badge { text: String, count: u32 }
impl Badge {
pub fn new(text: impl Into<String>) -> Self { /* … */ }
pub fn count(mut self, count: u32) -> Self { self.count = count; self }
#[cfg(feature = "egui")]
pub fn show(&self, ui: &mut egui::Ui, ctx: &UiContext) {
let color = components::token(ctx, &ctx.color_tokens().primary);
ui.colored_label(color, format!("{} ({})", self.text, self.count));
}
}- Add the path to
shared::api::endpointsand the request/response types tocrates/shared/src/api/mod.rs(bothserde::Serialize + Deserialize). - Handle it in
apps/server/src/lib.rs(match on the path, produce aResponse). - Call it from the client with
HttpNetworkService::get_json/post_json. - Both ends compile against the same type — rename a field and the other end stops compiling.
- Add a smoke check similar to the
backendCI job if the endpoint is user-facing.
- Define it in
crates/shared/src/domain/mod.rsand implementEntity(andRepositoryEntityif it will live in a repository). - Use
InMemoryRepository::<T>::new()for storage, or add a method to an existing service trait incrates/shared/src/services/mod.rsand implement it inservices::defaults. - Wire it into
ServiceContainer/AppServicesindefaults::*and inservices::factory::create_services. - Prefer returning
shared::errors::Result<T>(AppError) overanyhowinsideshared.
- If it is a contract (a method every platform answers): add it to
PlatformUi/Windowincrates/ui/src/platform/mod.rsand implement the default incrates/ui/src/platform/*.rs. - If it is real OS work (JNI,
Win32,uname,web-sys, haptics): implement it in the matchingcrates/platform/<os>/src/lib.rsbehind#[cfg(target_os = "…")]. - Expose a small imperative function (like
platform_android::device_info()) and call it from the app'sinit_platform/entry point, gated by#[cfg].
- Change tokens programmatically with
ThemeBuilder(primary_color,secondary_color,font_family,base_font_size,spacing_scale,border_radius_scale,motion_speed). - Or pick a built-in:
material3(),ios(),material_you(),high_contrast(),minimal(). - At runtime,
ThemeManagerupdates a live theme andgenerate_css()exports it for the web. - The user-facing theme choice lives in
AppConfig.ui.theme.mode(ThemeMode).
- Add the key to
resources/strings/strings_en.json(andstrings_es.json). - Read it with
ResourceManager::global().get_string("key", Some("en")), orget_string_formatted(…)for placeholders. ResourceManager::load_all()/reload()refresh the cache.
- One run:
cargo run --package example-app -- --server http://host:port(also--serverfordev.sh). - Permanently: change
network.base_urlin the config written byAppConfig::save(). - On Android,
adb reverse tcp:8080 tcp:8080first (device loopback ≠ host loopback).
- Follow
apps/example/src/backend.rs: spawn a worker thread, perform the blockingHttpNetworkServicecall there, and send the result back over anmpscchannel thatupdate()drains each frame. Never call the blocking client on the egui thread. - On the web, do not ship a fake client; render a placeholder (there are no raw sockets), or use
web_sys::fetchon awasm32code path.
- Rename the
[lib] nameinapps/example/Cargo.tomland update the two Android identifiers that must match it (AndroidManifest.xml'sandroid.app.lib_nameand Gradle'srustLibName). - Change
WINDOW_TITLEinapps/example/src/lib.rs. - Update
[workspace.package]metadata (repository,homepage,authors,description) in the rootCargo.toml. - Update
android-app's application id andresources/strings.
These are the rules that keep the workspace cross-compiling and the README's claims true.
- One-way dependencies.
shareddepends on nothing internal;ui/resources/build-configdepend only onshared;platform/*depend onshared+ui;apps/serverdepends onsharedonly. Never makesharedoruidepend on a platform crate — that creates a cycle, andcargo check --workspaceon a machine with no SDKs must keep working. sharedand the pure halves ofuicontain noeguiand no OS types.- Every component's pure logic compiles with
eguioff.cargo check -p ui --no-default-featuresmust stay green. - The wire contract lives only in
shared::api. Both ends import it; never duplicate DTOs. - Entry points that cannot exist on a target are
#[cfg]-gated, not runtime-branched. - Time goes through
web_time, neverstd::time::Instant::now()/SystemTime::now()directly, so the wasm target does not abort. - The Android library name is load-bearing: it must match in
Cargo.toml, the manifest and Gradle. - Clippy is
-D warningswithall,pedantic,nurseryandcargoat warn (workspace lints). New code must be lint-clean. Cargo.lockstays committed, andpanic = "abort"stays out of the release profile (it would makecargo test --releaseunusable).- The
networkfeature stays off by default andHttpNetworkServicestaysreqwest-free, so Android/web builds keep cross-compiling without a TLS/C toolchain.
| Symptom | Cause / fix |
|---|---|
web-sys "no method named class_list/style/head" |
web-sys generates a method only when its feature is on. Add the feature to the web-sys list in the root Cargo.toml. |
Android build fails to find the linker/cc |
NDK r23+ removed the bare <arch>-linux-android-clang symlinks. source scripts/android-env.sh (exports CARGO_TARGET_*_LINKER, CC_*, CXX_*, AR_*). |
A platform-windows (or platform-macos) error appears only when you target that OS, while cargo check --workspace on Linux is green |
Desktop code sits behind #[cfg(target_os = "windows")], so a Linux host never type-checks it. Cross-check without a linker: rustup target add x86_64-pc-windows-msvc then cargo check --workspace --target x86_64-pc-windows-msvc. Producing a real .exe (linking) still needs MinGW-w64 via scripts/build-windows.sh, or a Windows host. |
android_main code is not type-checked in the editor |
rust-analyzer analyzes for the host. Set rust-analyzer.cargo.target = "aarch64-linux-android" in your uncommitted .vscode/settings.json. |
Two incompatible JNIEnv types |
Two jni versions in the graph. jni = "0.22" must match what android-activity 0.6 resolves to. |
uuid/getrandom fails on wasm |
Entropy needs the js backend: getrandom { features = ["js"] } and uuid { features = ["v4","serde","js"] } for the wasm graph (already declared in apps/example/Cargo.toml). |
| Freeze/panic on time in the browser | Use web_time, not std::time, on wasm32. |
cargo <alias> recurses forever |
An alias may not share a name with a real Cargo subcommand. Keep using f/l, not fmt/clippy. |
| Backend unreachable from an emulator | 127.0.0.1 is the device. adb reverse tcp:8080 tcp:8080, or bind --bind 0.0.0.0:8080. |
ui methods missing under --no-default-features |
Expected: show(…) is behind the egui feature. |
hooks/utils look empty |
They exist as files but are not declared in ui/src/lib.rs — they are intentionally out of the egui stack for now. |
Concrete rules that make generated code correct in this repository:
- Locate first, then write. Use the tree in §4 and the map in §5. If a symbol is not there, it does not exist — create it in the crate that §3.1 says owns it.
- Respect the layers. Business logic →
shared. Reusable widget →ui::componentswith a pure-logic half and acfg(egui)show. OS call →crates/platform/*. App wiring →apps/example. Endpoint →shared::api+apps/server/src/lib.rs. - Keep the
egui-off build green. Noeguitype may appear outside#[cfg(feature = "egui")]blocks inui. - Prefer
shared::errors::Result/AppErrorinshared;anyhowis fine at the app edge. - Gate per-target code with
#[cfg], mirroring the existingrun/run_web/android_mainpattern. Never call an Android/JNI/web-sysAPI unguarded. - Do not add a second way to do an existing thing (a second HTTP client, a second theme system, a second router). Extend the existing seam.
- Run the gates in §10 before declaring a task done, and state honestly what you ran and what you did not (matching the project's own style).
- Comments explain why. The codebase's comments document the reasoning (why a
cfg, whyjlinkmatters, why an alias is short) — match that, not "what" comments.
- README.md — English quick start and project overview.
- README.fa.md — نسخهٔ فارسی.
- Wire contract:
crates/shared/src/api/mod.rs - Component library:
crates/ui/src/components/ - Platform traits:
crates/ui/src/platform/mod.rs - Entry points:
apps/example/src/lib.rs - Backend:
apps/server/src/lib.rs - CI gates:
.github/workflows/ci.yml
Author: Sina Khanzadeh — khanzadeh.1377@gmail.com — sina-khanzadeh.ir
Instagram / Telegram / Discord: programmer_1998
Licence: MIT OR Apache-2.0 — see Cargo.toml ([workspace.package] license).