Skip to main content

mobiler_ui/
lib.rs

1//! Mobiler's fixed UI wire ABI.
2//!
3//! These types are the **stable contract** between any Mobiler app's Rust core
4//! and the native shell. Because they never change per app, a single shell is
5//! built once and renders *any* Mobiler app — the shell only ever knows these
6//! types, never an app's domain events or widgets.
7//!
8//! - The core emits a [`Widget`] tree (the `ViewModel`).
9//! - The shell sends back an [`Action`] (the `Event`).
10//! - App domain events ride inside actions as opaque [`ActionToken`]s that the
11//!   shell round-trips without interpreting.
12//!
13//! Style is expressed as **intent tokens** (e.g. [`TextStyle`], [`Tone`]); the
14//! shell maps each to a concrete look (font, color, dp), so dark mode and theme
15//! come for free on the native side.
16
17use facet::Facet;
18use serde::{Deserialize, Serialize};
19
20/// An opaque, serialized app event (e.g. JSON of the app's domain action).
21pub type ActionToken = String;
22
23/// A value produced by an input widget at runtime.
24#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
25#[repr(C)]
26pub enum InputValue {
27    Text(String),
28    Bool(bool),
29    Int(i64),
30}
31
32/// What the shell sends back to the core. **Fixed across all apps.**
33#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
34#[repr(C)]
35pub enum Action {
36    /// An action widget (button/etc.) fired; `token` is the opaque app event.
37    Fired { token: ActionToken },
38    /// A value-carrying input changed; `id` names the widget.
39    Input { id: String, value: InputValue },
40    /// Persisted state handed back to the core on startup (empty string if none).
41    Restore { data: String },
42    /// Fired once on startup (after `Restore`) so the app can kick off initial
43    /// effects (e.g. fetching data).
44    Start,
45}
46
47// ---------------------------- style tokens ----------------------------
48
49#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
50#[repr(C)]
51pub enum TextStyle { Body, Title, Subtitle, Caption, Emphasis }
52
53#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
54#[repr(C)]
55pub enum ButtonStyle { Filled, Outlined, Text }
56
57#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
58#[repr(C)]
59pub enum CardStyle { Elevated, Outlined, Filled }
60
61/// Semantic status color (distinct from brand/identity color).
62#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
63#[repr(C)]
64pub enum Tone { Neutral, Success, Warning, Danger, Info }
65
66#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
67#[repr(C)]
68pub enum Spacing { Xs, Sm, Md, Lg, Xl }
69
70/// A small, finite icon set (maps to Material icons in the shell).
71#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
72#[repr(C)]
73pub enum Icon { Delete, Add, Edit, Close, Settings, Check, Star }
74
75#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
76#[repr(C)]
77pub enum ImageShape { Square, Rounded, Circle }
78
79#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
80#[repr(C)]
81pub enum ImageRatio { Wide, Square, Tall }
82
83#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
84#[repr(C)]
85pub enum BoxAlign { TopStart, TopEnd, Center, BottomStart, BottomCenter, BottomEnd }
86
87/// Project-identity colors (distinct from semantic `Tone`). Concrete RGB decided
88/// in the render layer.
89#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
90#[repr(C)]
91pub enum ProjectColor { Indigo, Teal, Coral, Amber, Lime, Pink }
92
93/// A bottom-navigation tab. `selected` marks the active one; tapping sends
94/// `on_select`.
95#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
96#[repr(C)]
97pub struct Tab {
98    pub label: String,
99    pub selected: bool,
100    pub on_select: ActionToken,
101}
102
103// ------------------------------- widgets -------------------------------
104
105/// The app-agnostic widget tree the shell renders. **Fixed across all apps.**
106#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
107#[repr(C)]
108pub enum Widget {
109    // Content
110    Text { content: String, style: TextStyle },
111    Image { source: String, shape: ImageShape, ratio: ImageRatio },
112    Badge { label: String, tone: Tone },
113    /// Small non-interactive colored dot — a project/identity hint.
114    ColorDot { color: ProjectColor },
115    Divider,
116    Spacer { size: Spacing },
117    // Layout
118    Row { children: Vec<Widget> },
119    Column { children: Vec<Widget> },
120    /// Card; tappable when `on_press` is set.
121    Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
122    /// Z-stack: children layered back-to-front, positioned by `align`. With
123    /// `scrim`, the first child is a background image, darkened for legibility,
124    /// and the rest render on top in light content.
125    Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
126    /// Fixed 2-column grid; children flow left-to-right, top-to-bottom.
127    Grid { children: Vec<Widget> },
128    // Input
129    Button { label: String, style: ButtonStyle, on_press: ActionToken },
130    IconButton { icon: Icon, on_press: ActionToken },
131    Chip { label: String, selected: bool, on_press: ActionToken },
132    TextField { id: String, placeholder: String, value: String },
133    Switch { id: String, label: String, value: bool },
134    Checkbox { id: String, label: String, value: bool },
135    /// Continuous 0..=`max` slider; emits `Input { id, Int }`.
136    Slider { id: String, value: i32, max: i32 },
137    /// Numeric stepper with −/+ controls carrying their own events.
138    Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
139    /// App shell: a top bar (`title` + optional `back`), a scrollable `body`,
140    /// and bottom-nav `tabs`. `dark_mode` is theme-as-data — the shell themes
141    /// the whole app from it.
142    ///
143    /// `route` + `depth` drive navigation: the shell animates the body when
144    /// `route` (the current screen's identity) changes — slide for push/pop
145    /// (direction from whether `depth` grew or shrank), crossfade for a lateral
146    /// move at the same depth — and wires the system back button to `back`.
147    Scaffold {
148        title: String,
149        body: Box<Widget>,
150        tabs: Vec<Tab>,
151        back: Option<ActionToken>,
152        dark_mode: bool,
153        route: String,
154        depth: u32,
155    },
156}