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}
43
44// ---------------------------- style tokens ----------------------------
45
46#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
47#[repr(C)]
48pub enum TextStyle { Body, Title, Subtitle, Caption, Emphasis }
49
50#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
51#[repr(C)]
52pub enum ButtonStyle { Filled, Outlined, Text }
53
54#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
55#[repr(C)]
56pub enum CardStyle { Elevated, Outlined, Filled }
57
58/// Semantic status color (distinct from brand/identity color).
59#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
60#[repr(C)]
61pub enum Tone { Neutral, Success, Warning, Danger, Info }
62
63#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
64#[repr(C)]
65pub enum Spacing { Xs, Sm, Md, Lg, Xl }
66
67/// A small, finite icon set (maps to Material icons in the shell).
68#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
69#[repr(C)]
70pub enum Icon { Delete, Add, Edit, Close, Settings, Check, Star }
71
72#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
73#[repr(C)]
74pub enum ImageShape { Square, Rounded, Circle }
75
76#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
77#[repr(C)]
78pub enum ImageRatio { Wide, Square, Tall }
79
80#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
81#[repr(C)]
82pub enum BoxAlign { TopStart, TopEnd, Center, BottomStart, BottomCenter, BottomEnd }
83
84/// Project-identity colors (distinct from semantic `Tone`). Concrete RGB decided
85/// in the render layer.
86#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
87#[repr(C)]
88pub enum ProjectColor { Indigo, Teal, Coral, Amber, Lime, Pink }
89
90/// A bottom-navigation tab. `selected` marks the active one; tapping sends
91/// `on_select`.
92#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
93#[repr(C)]
94pub struct Tab {
95    pub label: String,
96    pub selected: bool,
97    pub on_select: ActionToken,
98}
99
100// ------------------------------- widgets -------------------------------
101
102/// The app-agnostic widget tree the shell renders. **Fixed across all apps.**
103#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
104#[repr(C)]
105pub enum Widget {
106    // Content
107    Text { content: String, style: TextStyle },
108    Image { source: String, shape: ImageShape, ratio: ImageRatio },
109    Badge { label: String, tone: Tone },
110    /// Small non-interactive colored dot — a project/identity hint.
111    ColorDot { color: ProjectColor },
112    Divider,
113    Spacer { size: Spacing },
114    // Layout
115    Row { children: Vec<Widget> },
116    Column { children: Vec<Widget> },
117    /// Card; tappable when `on_press` is set.
118    Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
119    /// Z-stack: children layered back-to-front, positioned by `align`. With
120    /// `scrim`, the first child is a background image, darkened for legibility,
121    /// and the rest render on top in light content.
122    Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
123    /// Fixed 2-column grid; children flow left-to-right, top-to-bottom.
124    Grid { children: Vec<Widget> },
125    // Input
126    Button { label: String, style: ButtonStyle, on_press: ActionToken },
127    IconButton { icon: Icon, on_press: ActionToken },
128    Chip { label: String, selected: bool, on_press: ActionToken },
129    TextField { id: String, placeholder: String, value: String },
130    Switch { id: String, label: String, value: bool },
131    Checkbox { id: String, label: String, value: bool },
132    /// Continuous 0..=`max` slider; emits `Input { id, Int }`.
133    Slider { id: String, value: i32, max: i32 },
134    /// Numeric stepper with −/+ controls carrying their own events.
135    Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
136    /// App shell: a top bar (`title` + optional `back`), a scrollable `body`,
137    /// and bottom-nav `tabs`. `dark_mode` is theme-as-data — the shell themes
138    /// the whole app from it.
139    Scaffold {
140        title: String,
141        body: Box<Widget>,
142        tabs: Vec<Tab>,
143        back: Option<ActionToken>,
144        dark_mode: bool,
145    },
146}