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)]
77#[repr(C)]
78pub enum ChartStyle { Bar, Line, StackedBar, StackedBar100, Pie, Donut, Rings, Gauge }
79
80#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq)]
85#[repr(C)]
86pub struct ChartSeries {
87 pub name: String,
88 pub values: Vec<f32>,
89 pub color: Option<Rgb>,
90 pub goal: Option<f32>,
91}
92
93impl ChartSeries {
94 #[must_use]
96 pub fn new(name: impl Into<String>, values: Vec<f32>) -> Self {
97 Self { name: name.into(), values, color: None, goal: None }
98 }
99 #[must_use]
101 pub fn with_color(mut self, color: Rgb) -> Self {
102 self.color = Some(color);
103 self
104 }
105 #[must_use]
108 pub fn with_goal(mut self, goal: f32) -> Self {
109 self.goal = Some(goal);
110 self
111 }
112}
113
114#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq)]
125#[repr(C)]
126pub struct ChartRegion {
127 pub x0: f32,
128 pub x1: f32,
129 pub y0: f32,
130 pub y1: f32,
131 pub color: Option<Rgb>,
132 pub label: String,
133 pub vertical: bool,
134}
135
136#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq)]
139#[repr(C)]
140pub struct ChartRefLine {
141 pub value: f32,
142 pub label: String,
143 pub dashed: bool,
144}
145
146#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq)]
148#[repr(C)]
149pub struct ChartBracket {
150 pub y0: f32,
151 pub y1: f32,
152 pub label: String,
153}
154
155#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq)]
158#[repr(C)]
159pub struct ChartTick {
160 pub at: f32,
161 pub label: String,
162}
163
164#[derive(Facet, Serialize, Deserialize, Clone, Debug, PartialEq)]
166#[repr(C)]
167pub struct ChartLegendItem {
168 pub label: String,
169 pub color: Rgb,
170}
171
172impl ChartRegion {
173 #[must_use]
175 pub fn new(x0: f32, x1: f32, y0: f32, y1: f32, label: impl Into<String>) -> Self {
176 Self { x0, x1, y0, y1, color: None, label: label.into(), vertical: false }
177 }
178 #[must_use]
180 pub fn with_color(mut self, color: Rgb) -> Self {
181 self.color = Some(color);
182 self
183 }
184 #[must_use]
186 pub fn vertical(mut self) -> Self {
187 self.vertical = true;
188 self
189 }
190}
191
192impl ChartRefLine {
193 #[must_use]
195 pub fn target(value: f32, label: impl Into<String>) -> Self {
196 Self { value, label: label.into(), dashed: false }
197 }
198 #[must_use]
200 pub fn max(value: f32, label: impl Into<String>) -> Self {
201 Self { value, label: label.into(), dashed: true }
202 }
203}
204
205impl ChartTick {
206 #[must_use]
207 pub fn new(at: f32, label: impl Into<String>) -> Self {
208 Self { at, label: label.into() }
209 }
210}
211
212impl ChartLegendItem {
213 #[must_use]
214 pub fn new(label: impl Into<String>, color: Rgb) -> Self {
215 Self { label: label.into(), color }
216 }
217}
218
219impl ChartBracket {
220 #[must_use]
221 pub fn new(y0: f32, y1: f32, label: impl Into<String>) -> Self {
222 Self { y0, y1, label: label.into() }
223 }
224}
225
226#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
227#[repr(C)]
228pub enum Spacing { Xs, Sm, Md, Lg, Xl }
229
230#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
233#[repr(C)]
234pub enum Icon {
235 Delete, Add, Edit, Close, Settings, Check, Star, Info,
237 Home, Search, Menu, Filter, Back, Forward, Down, Bell, Cart, Share, Heart, HeartFilled,
239 Person, People, Phone, Mail, Calendar, Clock, MapPin,
241 Camera, Photo, Play,
243 Scissors,
245}
246
247#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
248#[repr(C)]
249pub enum ImageShape { Square, Rounded, Circle }
250
251#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
252#[repr(C)]
253pub enum ImageRatio { Wide, Square, Tall }
254
255#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
256#[repr(C)]
257pub enum BoxAlign { TopStart, TopEnd, Center, BottomStart, BottomCenter, BottomEnd }
258
259#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
262#[repr(C)]
263pub enum ProjectColor { Indigo, Teal, Coral, Amber, Lime, Pink }
264
265#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
270#[repr(C)]
271pub struct Rgb {
272 pub r: u8,
273 pub g: u8,
274 pub b: u8,
275}
276
277impl Rgb {
278 pub const fn new(r: u8, g: u8, b: u8) -> Self {
279 Self { r, g, b }
280 }
281}
282
283#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
285#[repr(C)]
286pub enum Corner { None, Small, Medium, Large }
287
288#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
290#[repr(C)]
291pub enum Density { Compact, Comfortable }
292
293#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
296#[repr(C)]
297pub enum FontFamily { System, Rounded, Serif, Monospace }
298
299#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
304#[repr(C)]
305pub struct Theme {
306 pub seed: Rgb,
307 pub accent: Option<Rgb>,
310 pub corner: Corner,
311 pub density: Density,
312 pub font: FontFamily,
313}
314
315impl Default for Theme {
319 fn default() -> Self {
320 Theme {
321 seed: Rgb::new(0x5C, 0x6B, 0xC0), accent: None,
323 corner: Corner::Medium,
324 density: Density::Comfortable,
325 font: FontFamily::System,
326 }
327 }
328}
329
330#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
333#[repr(C)]
334pub struct Tab {
335 pub label: String,
336 pub selected: bool,
337 pub on_select: ActionToken,
338 pub icon: Option<Icon>,
340}
341
342#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
344#[repr(C)]
345pub struct Fab {
346 pub icon: Icon,
347 pub on_press: ActionToken,
348}
349
350#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
353#[repr(C)]
354pub struct Segment {
355 pub label: String,
356 pub selected: bool,
357 pub on_select: ActionToken,
358}
359
360#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
363#[repr(C)]
364pub struct Sheet {
365 pub title: String,
366 pub child: Box<Widget>,
367 pub on_dismiss: ActionToken,
368}
369
370#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
372#[repr(C)]
373pub struct SwipeButton {
374 pub label: String,
375 pub tone: Tone,
376 pub on_tap: ActionToken,
377}
378
379#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
383#[repr(C)]
384pub enum Widget {
385 Text { content: String, style: TextStyle },
387 Image { source: String, shape: ImageShape, ratio: ImageRatio },
388 Badge { label: String, tone: Tone },
389 Avatar { source: String, status: Option<Tone> },
391 Rating { value: u32, max: u8, on_rate: Option<Vec<ActionToken>> },
394 ColorDot { color: ProjectColor },
396 Divider,
397 Progress { value: Option<f32> },
399 Skeleton,
401 Chart { series: Vec<ChartSeries>, labels: Vec<String>, style: ChartStyle, axis: bool, legend: bool },
405 RegionChart {
411 regions: Vec<ChartRegion>,
412 ticks: Vec<ChartTick>,
413 x_max: f32,
414 y_max: f32,
415 ref_lines: Vec<ChartRefLine>,
416 bracket: Option<ChartBracket>,
417 legend: Vec<ChartLegendItem>,
418 },
419 Calendar { year: u32, month: u8, first_weekday: u8, selected: Option<u8>, on_day: Vec<ActionToken> },
423 SwipeAction { child: Box<Widget>, actions: Vec<SwipeButton> },
426 Spacer { size: Spacing },
427 Row { children: Vec<Widget> },
429 Column { children: Vec<Widget> },
430 Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
432 Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
436 Grid { children: Vec<Widget> },
438 Scroller { children: Vec<Widget> },
440 Button { label: String, style: ButtonStyle, on_press: ActionToken },
442 IconButton { icon: Icon, on_press: ActionToken },
443 Chip { label: String, selected: bool, on_press: ActionToken },
444 TextField { id: String, placeholder: String, value: String },
445 SearchField { id: String, placeholder: String, value: String },
447 Segmented { segments: Vec<Segment> },
449 Toggle { id: String, label: String, value: bool },
450 Checkbox { id: String, label: String, value: bool },
451 Slider { id: String, value: i32, max: i32 },
453 Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
455 Scaffold {
464 title: String,
465 body: Box<Widget>,
466 tabs: Vec<Tab>,
467 back: Option<ActionToken>,
468 dark_mode: bool,
469 theme: Option<Theme>,
472 fab: Option<Fab>,
474 sheet: Option<Sheet>,
476 on_refresh: Option<ActionToken>,
480 refreshing: bool,
481 route: String,
482 depth: u32,
483 },
484}
485
486#[cfg(test)]
487mod tests {
488 use super::*;
489 use serde::Serialize;
490 use serde::de::DeserializeOwned;
491
492 fn round_trips<T: Serialize + DeserializeOwned>(value: &T) {
495 let a = serde_json::to_string(value).expect("serialize");
496 let back: T = serde_json::from_str(&a).expect("deserialize");
497 let b = serde_json::to_string(&back).expect("re-serialize");
498 assert_eq!(a, b);
499 }
500
501 #[test]
502 fn action_round_trips() {
503 round_trips(&Action::Start);
504 round_trips(&Action::Fired { token: "tok".to_string() });
505 round_trips(&Action::Input { id: "field".to_string(), value: InputValue::Bool(true) });
506 round_trips(&Action::Restore { data: "{}".to_string() });
507 }
508
509 #[test]
510 fn widget_round_trips() {
511 round_trips(&Widget::Text { content: "hi".to_string(), style: TextStyle::Title });
512 round_trips(&Widget::ColorDot { color: ProjectColor::Teal });
513 round_trips(&Widget::Chart {
514 series: vec![ChartSeries { name: "s".to_string(), values: vec![1.0, 2.5, 3.0], color: None, goal: None }],
515 labels: vec!["a".to_string()],
516 style: ChartStyle::Bar,
517 axis: true,
518 legend: false,
519 });
520 round_trips(&Widget::RegionChart {
521 regions: vec![ChartRegion::new(0.0, 3.0, 0.0, 80.0, "80%").vertical(),
522 ChartRegion::new(3.0, 21.0, 0.0, 80.0, "CHF 80'000").with_color(Rgb::new(0x8E, 0xC6, 0xBA))],
523 ticks: vec![ChartTick { at: 3.0, label: "3 Mt.".to_string() }, ChartTick { at: 65.0, label: "65 J.".to_string() }],
524 x_max: 65.0,
525 y_max: 80.0,
526 ref_lines: vec![ChartRefLine { value: 80.0, label: "CHF 80'000".to_string(), dashed: false }],
527 bracket: Some(ChartBracket { y0: 60.0, y1: 80.0, label: "Ceiling".to_string() }),
528 legend: vec![ChartLegendItem { label: "Gap".to_string(), color: Rgb::new(0x5A, 0x7D, 0x9A) }],
529 });
530 round_trips(&Widget::Calendar { year: 2026, month: 6, first_weekday: 1, selected: Some(15), on_day: vec!["d1".to_string(), "d2".to_string()] });
531 round_trips(&Widget::SwipeAction { child: Box::new(Widget::Divider), actions: vec![SwipeButton { label: "Del".to_string(), tone: Tone::Danger, on_tap: "t".to_string() }] });
532 round_trips(&Widget::Scaffold {
534 title: "T".to_string(),
535 body: Box::new(Widget::Divider),
536 tabs: vec![Tab { label: "A".to_string(), selected: true, on_select: "t".to_string(), icon: Some(Icon::Home) }],
537 back: Some("b".to_string()),
538 dark_mode: true,
539 theme: None,
540 fab: None,
541 sheet: None,
542 on_refresh: None,
543 refreshing: false,
544 route: "r".to_string(),
545 depth: 2,
546 });
547 round_trips(&Widget::Scaffold {
549 title: "T".to_string(),
550 body: Box::new(Widget::Divider),
551 tabs: vec![],
552 back: None,
553 dark_mode: false,
554 theme: Some(Theme {
555 seed: Rgb::new(0xC8, 0x5A, 0x3C),
556 accent: Some(Rgb::new(0xE0, 0x6A, 0x2C)),
557 corner: Corner::Large,
558 density: Density::Compact,
559 font: FontFamily::Rounded,
560 }),
561 fab: Some(Fab { icon: Icon::Calendar, on_press: "f".to_string() }),
562 sheet: Some(Sheet { title: "S".to_string(), child: Box::new(Widget::Divider), on_dismiss: "d".to_string() }),
563 on_refresh: Some("r".to_string()),
564 refreshing: true,
565 route: "r".to_string(),
566 depth: 1,
567 });
568 }
569}