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)]
67#[repr(C)]
68pub enum Spacing { Xs, Sm, Md, Lg, Xl }
69
70#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
73#[repr(C)]
74pub enum Icon {
75 Delete, Add, Edit, Close, Settings, Check, Star, Info,
77 Home, Search, Menu, Filter, Back, Forward, Down, Bell, Cart, Share, Heart, HeartFilled,
79 Person, People, Phone, Mail, Calendar, Clock, MapPin,
81 Camera, Photo, Play,
83 Scissors,
85}
86
87#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
88#[repr(C)]
89pub enum ImageShape { Square, Rounded, Circle }
90
91#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
92#[repr(C)]
93pub enum ImageRatio { Wide, Square, Tall }
94
95#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
96#[repr(C)]
97pub enum BoxAlign { TopStart, TopEnd, Center, BottomStart, BottomCenter, BottomEnd }
98
99#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
102#[repr(C)]
103pub enum ProjectColor { Indigo, Teal, Coral, Amber, Lime, Pink }
104
105#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
110#[repr(C)]
111pub struct Rgb {
112 pub r: u8,
113 pub g: u8,
114 pub b: u8,
115}
116
117impl Rgb {
118 pub const fn new(r: u8, g: u8, b: u8) -> Self {
119 Self { r, g, b }
120 }
121}
122
123#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
125#[repr(C)]
126pub enum Corner { None, Small, Medium, Large }
127
128#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
130#[repr(C)]
131pub enum Density { Compact, Comfortable }
132
133#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
136#[repr(C)]
137pub enum FontFamily { System, Rounded, Serif, Monospace }
138
139#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
144#[repr(C)]
145pub struct Theme {
146 pub seed: Rgb,
147 pub accent: Option<Rgb>,
150 pub corner: Corner,
151 pub density: Density,
152 pub font: FontFamily,
153}
154
155impl Default for Theme {
159 fn default() -> Self {
160 Theme {
161 seed: Rgb::new(0x5C, 0x6B, 0xC0), accent: None,
163 corner: Corner::Medium,
164 density: Density::Comfortable,
165 font: FontFamily::System,
166 }
167 }
168}
169
170#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
173#[repr(C)]
174pub struct Tab {
175 pub label: String,
176 pub selected: bool,
177 pub on_select: ActionToken,
178 pub icon: Option<Icon>,
180}
181
182#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
184#[repr(C)]
185pub struct Fab {
186 pub icon: Icon,
187 pub on_press: ActionToken,
188}
189
190#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
193#[repr(C)]
194pub struct Segment {
195 pub label: String,
196 pub selected: bool,
197 pub on_select: ActionToken,
198}
199
200#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
203#[repr(C)]
204pub struct Sheet {
205 pub title: String,
206 pub child: Box<Widget>,
207 pub on_dismiss: ActionToken,
208}
209
210#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
214#[repr(C)]
215pub enum Widget {
216 Text { content: String, style: TextStyle },
218 Image { source: String, shape: ImageShape, ratio: ImageRatio },
219 Badge { label: String, tone: Tone },
220 Avatar { source: String, status: Option<Tone> },
222 Rating { value: u32, max: u8, on_rate: Option<Vec<ActionToken>> },
225 ColorDot { color: ProjectColor },
227 Divider,
228 Progress { value: Option<f32> },
230 Skeleton,
232 Spacer { size: Spacing },
233 Row { children: Vec<Widget> },
235 Column { children: Vec<Widget> },
236 Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
238 Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
242 Grid { children: Vec<Widget> },
244 Scroller { children: Vec<Widget> },
246 Button { label: String, style: ButtonStyle, on_press: ActionToken },
248 IconButton { icon: Icon, on_press: ActionToken },
249 Chip { label: String, selected: bool, on_press: ActionToken },
250 TextField { id: String, placeholder: String, value: String },
251 SearchField { id: String, placeholder: String, value: String },
253 Segmented { segments: Vec<Segment> },
255 Toggle { id: String, label: String, value: bool },
256 Checkbox { id: String, label: String, value: bool },
257 Slider { id: String, value: i32, max: i32 },
259 Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
261 Scaffold {
270 title: String,
271 body: Box<Widget>,
272 tabs: Vec<Tab>,
273 back: Option<ActionToken>,
274 dark_mode: bool,
275 theme: Option<Theme>,
278 fab: Option<Fab>,
280 sheet: Option<Sheet>,
282 route: String,
283 depth: u32,
284 },
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use serde::Serialize;
291 use serde::de::DeserializeOwned;
292
293 fn round_trips<T: Serialize + DeserializeOwned>(value: &T) {
296 let a = serde_json::to_string(value).expect("serialize");
297 let back: T = serde_json::from_str(&a).expect("deserialize");
298 let b = serde_json::to_string(&back).expect("re-serialize");
299 assert_eq!(a, b);
300 }
301
302 #[test]
303 fn action_round_trips() {
304 round_trips(&Action::Start);
305 round_trips(&Action::Fired { token: "tok".to_string() });
306 round_trips(&Action::Input { id: "field".to_string(), value: InputValue::Bool(true) });
307 round_trips(&Action::Restore { data: "{}".to_string() });
308 }
309
310 #[test]
311 fn widget_round_trips() {
312 round_trips(&Widget::Text { content: "hi".to_string(), style: TextStyle::Title });
313 round_trips(&Widget::ColorDot { color: ProjectColor::Teal });
314 round_trips(&Widget::Scaffold {
316 title: "T".to_string(),
317 body: Box::new(Widget::Divider),
318 tabs: vec![Tab { label: "A".to_string(), selected: true, on_select: "t".to_string(), icon: Some(Icon::Home) }],
319 back: Some("b".to_string()),
320 dark_mode: true,
321 theme: None,
322 fab: None,
323 sheet: None,
324 route: "r".to_string(),
325 depth: 2,
326 });
327 round_trips(&Widget::Scaffold {
329 title: "T".to_string(),
330 body: Box::new(Widget::Divider),
331 tabs: vec![],
332 back: None,
333 dark_mode: false,
334 theme: Some(Theme {
335 seed: Rgb::new(0xC8, 0x5A, 0x3C),
336 accent: Some(Rgb::new(0xE0, 0x6A, 0x2C)),
337 corner: Corner::Large,
338 density: Density::Compact,
339 font: FontFamily::Rounded,
340 }),
341 fab: Some(Fab { icon: Icon::Calendar, on_press: "f".to_string() }),
342 sheet: Some(Sheet { title: "S".to_string(), child: Box::new(Widget::Divider), on_dismiss: "d".to_string() }),
343 route: "r".to_string(),
344 depth: 1,
345 });
346 }
347}