1use facet::Facet;
18use serde::{Deserialize, Serialize};
19
20pub type ActionToken = String;
22
23#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
25#[repr(C)]
26pub enum InputValue {
27 Text(String),
28 Bool(bool),
29 Int(i64),
30}
31
32#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
34#[repr(C)]
35pub enum Action {
36 Fired { token: ActionToken },
38 Input { id: String, value: InputValue },
40 Restore { data: String },
42 Start,
45}
46
47#[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#[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#[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#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
90#[repr(C)]
91pub enum ProjectColor { Indigo, Teal, Coral, Amber, Lime, Pink }
92
93#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
98#[repr(C)]
99pub struct Rgb {
100 pub r: u8,
101 pub g: u8,
102 pub b: u8,
103}
104
105impl Rgb {
106 pub const fn new(r: u8, g: u8, b: u8) -> Self {
107 Self { r, g, b }
108 }
109}
110
111#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
113#[repr(C)]
114pub enum Corner { None, Small, Medium, Large }
115
116#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
118#[repr(C)]
119pub enum Density { Compact, Comfortable }
120
121#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
124#[repr(C)]
125pub enum FontFamily { System, Rounded, Serif, Monospace }
126
127#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
132#[repr(C)]
133pub struct Theme {
134 pub seed: Rgb,
135 pub corner: Corner,
136 pub density: Density,
137 pub font: FontFamily,
138}
139
140impl Default for Theme {
144 fn default() -> Self {
145 Theme {
146 seed: Rgb::new(0x5C, 0x6B, 0xC0), corner: Corner::Medium,
148 density: Density::Comfortable,
149 font: FontFamily::System,
150 }
151 }
152}
153
154#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
157#[repr(C)]
158pub struct Tab {
159 pub label: String,
160 pub selected: bool,
161 pub on_select: ActionToken,
162}
163
164#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
168#[repr(C)]
169pub enum Widget {
170 Text { content: String, style: TextStyle },
172 Image { source: String, shape: ImageShape, ratio: ImageRatio },
173 Badge { label: String, tone: Tone },
174 ColorDot { color: ProjectColor },
176 Divider,
177 Spacer { size: Spacing },
178 Row { children: Vec<Widget> },
180 Column { children: Vec<Widget> },
181 Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
183 Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
187 Grid { children: Vec<Widget> },
189 Button { label: String, style: ButtonStyle, on_press: ActionToken },
191 IconButton { icon: Icon, on_press: ActionToken },
192 Chip { label: String, selected: bool, on_press: ActionToken },
193 TextField { id: String, placeholder: String, value: String },
194 Toggle { id: String, label: String, value: bool },
195 Checkbox { id: String, label: String, value: bool },
196 Slider { id: String, value: i32, max: i32 },
198 Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
200 Scaffold {
209 title: String,
210 body: Box<Widget>,
211 tabs: Vec<Tab>,
212 back: Option<ActionToken>,
213 dark_mode: bool,
214 theme: Option<Theme>,
217 route: String,
218 depth: u32,
219 },
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225 use serde::Serialize;
226 use serde::de::DeserializeOwned;
227
228 fn round_trips<T: Serialize + DeserializeOwned>(value: &T) {
231 let a = serde_json::to_string(value).expect("serialize");
232 let back: T = serde_json::from_str(&a).expect("deserialize");
233 let b = serde_json::to_string(&back).expect("re-serialize");
234 assert_eq!(a, b);
235 }
236
237 #[test]
238 fn action_round_trips() {
239 round_trips(&Action::Start);
240 round_trips(&Action::Fired { token: "tok".to_string() });
241 round_trips(&Action::Input { id: "field".to_string(), value: InputValue::Bool(true) });
242 round_trips(&Action::Restore { data: "{}".to_string() });
243 }
244
245 #[test]
246 fn widget_round_trips() {
247 round_trips(&Widget::Text { content: "hi".to_string(), style: TextStyle::Title });
248 round_trips(&Widget::ColorDot { color: ProjectColor::Teal });
249 round_trips(&Widget::Scaffold {
251 title: "T".to_string(),
252 body: Box::new(Widget::Divider),
253 tabs: vec![Tab { label: "A".to_string(), selected: true, on_select: "t".to_string() }],
254 back: Some("b".to_string()),
255 dark_mode: true,
256 theme: None,
257 route: "r".to_string(),
258 depth: 2,
259 });
260 round_trips(&Widget::Scaffold {
262 title: "T".to_string(),
263 body: Box::new(Widget::Divider),
264 tabs: vec![],
265 back: None,
266 dark_mode: false,
267 theme: Some(Theme {
268 seed: Rgb::new(0xC8, 0x5A, 0x3C),
269 corner: Corner::Large,
270 density: Density::Compact,
271 font: FontFamily::Rounded,
272 }),
273 route: "r".to_string(),
274 depth: 1,
275 });
276 }
277}