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, Brand }
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)]
68#[repr(C)]
69pub enum ChartStyle { Bar, Line }
70
71#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
72#[repr(C)]
73pub enum Spacing { Xs, Sm, Md, Lg, Xl }
74
75#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
78#[repr(C)]
79pub enum Icon {
80 Delete, Add, Edit, Close, Settings, Check, Star, Info,
82 Home, Search, Menu, Filter, Back, Forward, Down, Bell, Cart, Share, Heart, HeartFilled,
84 Person, People, Phone, Mail, Calendar, Clock, MapPin,
86 Camera, Photo, Play,
88 Scissors,
90}
91
92#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
93#[repr(C)]
94pub enum ImageShape { Square, Rounded, Circle }
95
96#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
97#[repr(C)]
98pub enum ImageRatio { Wide, Square, Tall }
99
100#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
101#[repr(C)]
102pub enum BoxAlign { TopStart, TopEnd, Center, BottomStart, BottomCenter, BottomEnd }
103
104#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
107#[repr(C)]
108pub enum ProjectColor { Indigo, Teal, Coral, Amber, Lime, Pink }
109
110#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
115#[repr(C)]
116pub struct Rgb {
117 pub r: u8,
118 pub g: u8,
119 pub b: u8,
120}
121
122impl Rgb {
123 pub const fn new(r: u8, g: u8, b: u8) -> Self {
124 Self { r, g, b }
125 }
126}
127
128#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
130#[repr(C)]
131pub enum Corner { None, Small, Medium, Large }
132
133#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
135#[repr(C)]
136pub enum Density { Compact, Comfortable }
137
138#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
141#[repr(C)]
142pub enum FontFamily { System, Rounded, Serif, Monospace }
143
144#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
149#[repr(C)]
150pub struct Theme {
151 pub seed: Rgb,
152 pub accent: Option<Rgb>,
155 pub corner: Corner,
156 pub density: Density,
157 pub font: FontFamily,
158}
159
160impl Default for Theme {
164 fn default() -> Self {
165 Theme {
166 seed: Rgb::new(0x5C, 0x6B, 0xC0), accent: None,
168 corner: Corner::Medium,
169 density: Density::Comfortable,
170 font: FontFamily::System,
171 }
172 }
173}
174
175#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
178#[repr(C)]
179pub struct Tab {
180 pub label: String,
181 pub selected: bool,
182 pub on_select: ActionToken,
183 pub icon: Option<Icon>,
185}
186
187#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
189#[repr(C)]
190pub struct Fab {
191 pub icon: Icon,
192 pub on_press: ActionToken,
193}
194
195#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
198#[repr(C)]
199pub struct Segment {
200 pub label: String,
201 pub selected: bool,
202 pub on_select: ActionToken,
203}
204
205#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
208#[repr(C)]
209pub struct Sheet {
210 pub title: String,
211 pub child: Box<Widget>,
212 pub on_dismiss: ActionToken,
213}
214
215#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
217#[repr(C)]
218pub struct SwipeButton {
219 pub label: String,
220 pub tone: Tone,
221 pub on_tap: ActionToken,
222}
223
224#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
228#[repr(C)]
229pub enum Widget {
230 Text { content: String, style: TextStyle },
232 Image { source: String, shape: ImageShape, ratio: ImageRatio },
233 Badge { label: String, tone: Tone },
234 Avatar { source: String, status: Option<Tone> },
236 Rating { value: u32, max: u8, on_rate: Option<Vec<ActionToken>> },
239 ColorDot { color: ProjectColor },
241 Divider,
242 Progress { value: Option<f32> },
244 Skeleton,
246 Chart { values: Vec<f32>, labels: Vec<String>, style: ChartStyle },
249 Calendar { year: u32, month: u8, first_weekday: u8, selected: Option<u8>, on_day: Vec<ActionToken> },
253 SwipeAction { child: Box<Widget>, actions: Vec<SwipeButton> },
256 Spacer { size: Spacing },
257 Row { children: Vec<Widget> },
259 Column { children: Vec<Widget> },
260 Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
262 Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
266 Grid { children: Vec<Widget> },
268 Scroller { children: Vec<Widget> },
270 Button { label: String, style: ButtonStyle, on_press: ActionToken },
272 IconButton { icon: Icon, on_press: ActionToken },
273 Chip { label: String, selected: bool, on_press: ActionToken },
274 TextField { id: String, placeholder: String, value: String },
275 SearchField { id: String, placeholder: String, value: String },
277 Segmented { segments: Vec<Segment> },
279 Toggle { id: String, label: String, value: bool },
280 Checkbox { id: String, label: String, value: bool },
281 Slider { id: String, value: i32, max: i32 },
283 Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
285 Scaffold {
294 title: String,
295 body: Box<Widget>,
296 tabs: Vec<Tab>,
297 back: Option<ActionToken>,
298 dark_mode: bool,
299 theme: Option<Theme>,
302 fab: Option<Fab>,
304 sheet: Option<Sheet>,
306 on_refresh: Option<ActionToken>,
310 refreshing: bool,
311 route: String,
312 depth: u32,
313 },
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use serde::Serialize;
320 use serde::de::DeserializeOwned;
321
322 fn round_trips<T: Serialize + DeserializeOwned>(value: &T) {
325 let a = serde_json::to_string(value).expect("serialize");
326 let back: T = serde_json::from_str(&a).expect("deserialize");
327 let b = serde_json::to_string(&back).expect("re-serialize");
328 assert_eq!(a, b);
329 }
330
331 #[test]
332 fn action_round_trips() {
333 round_trips(&Action::Start);
334 round_trips(&Action::Fired { token: "tok".to_string() });
335 round_trips(&Action::Input { id: "field".to_string(), value: InputValue::Bool(true) });
336 round_trips(&Action::Restore { data: "{}".to_string() });
337 }
338
339 #[test]
340 fn widget_round_trips() {
341 round_trips(&Widget::Text { content: "hi".to_string(), style: TextStyle::Title });
342 round_trips(&Widget::ColorDot { color: ProjectColor::Teal });
343 round_trips(&Widget::Chart { values: vec![1.0, 2.5, 3.0], labels: vec!["a".to_string()], style: ChartStyle::Bar });
344 round_trips(&Widget::Calendar { year: 2026, month: 6, first_weekday: 1, selected: Some(15), on_day: vec!["d1".to_string(), "d2".to_string()] });
345 round_trips(&Widget::SwipeAction { child: Box::new(Widget::Divider), actions: vec![SwipeButton { label: "Del".to_string(), tone: Tone::Danger, on_tap: "t".to_string() }] });
346 round_trips(&Widget::Scaffold {
348 title: "T".to_string(),
349 body: Box::new(Widget::Divider),
350 tabs: vec![Tab { label: "A".to_string(), selected: true, on_select: "t".to_string(), icon: Some(Icon::Home) }],
351 back: Some("b".to_string()),
352 dark_mode: true,
353 theme: None,
354 fab: None,
355 sheet: None,
356 on_refresh: None,
357 refreshing: false,
358 route: "r".to_string(),
359 depth: 2,
360 });
361 round_trips(&Widget::Scaffold {
363 title: "T".to_string(),
364 body: Box::new(Widget::Divider),
365 tabs: vec![],
366 back: None,
367 dark_mode: false,
368 theme: Some(Theme {
369 seed: Rgb::new(0xC8, 0x5A, 0x3C),
370 accent: Some(Rgb::new(0xE0, 0x6A, 0x2C)),
371 corner: Corner::Large,
372 density: Density::Compact,
373 font: FontFamily::Rounded,
374 }),
375 fab: Some(Fab { icon: Icon::Calendar, on_press: "f".to_string() }),
376 sheet: Some(Sheet { title: "S".to_string(), child: Box::new(Widget::Divider), on_dismiss: "d".to_string() }),
377 on_refresh: Some("r".to_string()),
378 refreshing: true,
379 route: "r".to_string(),
380 depth: 1,
381 });
382 }
383}