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 Spacer { size: Spacing },
229 Row { children: Vec<Widget> },
231 Column { children: Vec<Widget> },
232 Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
234 Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
238 Grid { children: Vec<Widget> },
240 Scroller { children: Vec<Widget> },
242 Button { label: String, style: ButtonStyle, on_press: ActionToken },
244 IconButton { icon: Icon, on_press: ActionToken },
245 Chip { label: String, selected: bool, on_press: ActionToken },
246 TextField { id: String, placeholder: String, value: String },
247 SearchField { id: String, placeholder: String, value: String },
249 Segmented { segments: Vec<Segment> },
251 Toggle { id: String, label: String, value: bool },
252 Checkbox { id: String, label: String, value: bool },
253 Slider { id: String, value: i32, max: i32 },
255 Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
257 Scaffold {
266 title: String,
267 body: Box<Widget>,
268 tabs: Vec<Tab>,
269 back: Option<ActionToken>,
270 dark_mode: bool,
271 theme: Option<Theme>,
274 fab: Option<Fab>,
276 sheet: Option<Sheet>,
278 route: String,
279 depth: u32,
280 },
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286 use serde::Serialize;
287 use serde::de::DeserializeOwned;
288
289 fn round_trips<T: Serialize + DeserializeOwned>(value: &T) {
292 let a = serde_json::to_string(value).expect("serialize");
293 let back: T = serde_json::from_str(&a).expect("deserialize");
294 let b = serde_json::to_string(&back).expect("re-serialize");
295 assert_eq!(a, b);
296 }
297
298 #[test]
299 fn action_round_trips() {
300 round_trips(&Action::Start);
301 round_trips(&Action::Fired { token: "tok".to_string() });
302 round_trips(&Action::Input { id: "field".to_string(), value: InputValue::Bool(true) });
303 round_trips(&Action::Restore { data: "{}".to_string() });
304 }
305
306 #[test]
307 fn widget_round_trips() {
308 round_trips(&Widget::Text { content: "hi".to_string(), style: TextStyle::Title });
309 round_trips(&Widget::ColorDot { color: ProjectColor::Teal });
310 round_trips(&Widget::Scaffold {
312 title: "T".to_string(),
313 body: Box::new(Widget::Divider),
314 tabs: vec![Tab { label: "A".to_string(), selected: true, on_select: "t".to_string(), icon: Some(Icon::Home) }],
315 back: Some("b".to_string()),
316 dark_mode: true,
317 theme: None,
318 fab: None,
319 sheet: None,
320 route: "r".to_string(),
321 depth: 2,
322 });
323 round_trips(&Widget::Scaffold {
325 title: "T".to_string(),
326 body: Box::new(Widget::Divider),
327 tabs: vec![],
328 back: None,
329 dark_mode: false,
330 theme: Some(Theme {
331 seed: Rgb::new(0xC8, 0x5A, 0x3C),
332 accent: Some(Rgb::new(0xE0, 0x6A, 0x2C)),
333 corner: Corner::Large,
334 density: Density::Compact,
335 font: FontFamily::Rounded,
336 }),
337 fab: Some(Fab { icon: Icon::Calendar, on_press: "f".to_string() }),
338 sheet: Some(Sheet { title: "S".to_string(), child: Box::new(Widget::Divider), on_dismiss: "d".to_string() }),
339 route: "r".to_string(),
340 depth: 1,
341 });
342 }
343}