use facet::Facet;
use serde::{Deserialize, Serialize};
pub type ActionToken = String;
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub enum InputValue {
Text(String),
Bool(bool),
Int(i64),
}
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub enum Action {
Fired { token: ActionToken },
Input { id: String, value: InputValue },
Restore { data: String },
Start,
}
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum TextStyle { Body, Title, Subtitle, Caption, Emphasis }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum ButtonStyle { Filled, Outlined, Text }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum CardStyle { Elevated, Outlined, Filled, Brand }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum Tone { Neutral, Success, Warning, Danger, Info }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum ChartStyle { Bar, Line }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum Spacing { Xs, Sm, Md, Lg, Xl }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum Icon {
Delete, Add, Edit, Close, Settings, Check, Star, Info,
Home, Search, Menu, Filter, Back, Forward, Down, Bell, Cart, Share, Heart, HeartFilled,
Person, People, Phone, Mail, Calendar, Clock, MapPin,
Camera, Photo, Play,
Scissors,
}
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum ImageShape { Square, Rounded, Circle }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum ImageRatio { Wide, Square, Tall }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum BoxAlign { TopStart, TopEnd, Center, BottomStart, BottomCenter, BottomEnd }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum ProjectColor { Indigo, Teal, Coral, Amber, Lime, Pink }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct Rgb {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl Rgb {
pub const fn new(r: u8, g: u8, b: u8) -> Self {
Self { r, g, b }
}
}
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum Corner { None, Small, Medium, Large }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum Density { Compact, Comfortable }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub enum FontFamily { System, Rounded, Serif, Monospace }
#[derive(Facet, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(C)]
pub struct Theme {
pub seed: Rgb,
pub accent: Option<Rgb>,
pub corner: Corner,
pub density: Density,
pub font: FontFamily,
}
impl Default for Theme {
fn default() -> Self {
Theme {
seed: Rgb::new(0x5C, 0x6B, 0xC0), accent: None,
corner: Corner::Medium,
density: Density::Comfortable,
font: FontFamily::System,
}
}
}
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub struct Tab {
pub label: String,
pub selected: bool,
pub on_select: ActionToken,
pub icon: Option<Icon>,
}
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub struct Fab {
pub icon: Icon,
pub on_press: ActionToken,
}
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub struct Segment {
pub label: String,
pub selected: bool,
pub on_select: ActionToken,
}
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub struct Sheet {
pub title: String,
pub child: Box<Widget>,
pub on_dismiss: ActionToken,
}
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub struct SwipeButton {
pub label: String,
pub tone: Tone,
pub on_tap: ActionToken,
}
#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
#[repr(C)]
pub enum Widget {
Text { content: String, style: TextStyle },
Image { source: String, shape: ImageShape, ratio: ImageRatio },
Badge { label: String, tone: Tone },
Avatar { source: String, status: Option<Tone> },
Rating { value: u32, max: u8, on_rate: Option<Vec<ActionToken>> },
ColorDot { color: ProjectColor },
Divider,
Progress { value: Option<f32> },
Skeleton,
Chart { values: Vec<f32>, labels: Vec<String>, style: ChartStyle },
Calendar { year: u32, month: u8, first_weekday: u8, selected: Option<u8>, on_day: Vec<ActionToken> },
SwipeAction { child: Box<Widget>, actions: Vec<SwipeButton> },
Spacer { size: Spacing },
Row { children: Vec<Widget> },
Column { children: Vec<Widget> },
Card { child: Box<Widget>, style: CardStyle, on_press: Option<ActionToken> },
Box { children: Vec<Widget>, align: BoxAlign, scrim: bool },
Grid { children: Vec<Widget> },
Scroller { children: Vec<Widget> },
Button { label: String, style: ButtonStyle, on_press: ActionToken },
IconButton { icon: Icon, on_press: ActionToken },
Chip { label: String, selected: bool, on_press: ActionToken },
TextField { id: String, placeholder: String, value: String },
SearchField { id: String, placeholder: String, value: String },
Segmented { segments: Vec<Segment> },
Toggle { id: String, label: String, value: bool },
Checkbox { id: String, label: String, value: bool },
Slider { id: String, value: i32, max: i32 },
Stepper { value: i32, on_decrement: ActionToken, on_increment: ActionToken },
Scaffold {
title: String,
body: Box<Widget>,
tabs: Vec<Tab>,
back: Option<ActionToken>,
dark_mode: bool,
theme: Option<Theme>,
fab: Option<Fab>,
sheet: Option<Sheet>,
on_refresh: Option<ActionToken>,
refreshing: bool,
route: String,
depth: u32,
},
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Serialize;
use serde::de::DeserializeOwned;
fn round_trips<T: Serialize + DeserializeOwned>(value: &T) {
let a = serde_json::to_string(value).expect("serialize");
let back: T = serde_json::from_str(&a).expect("deserialize");
let b = serde_json::to_string(&back).expect("re-serialize");
assert_eq!(a, b);
}
#[test]
fn action_round_trips() {
round_trips(&Action::Start);
round_trips(&Action::Fired { token: "tok".to_string() });
round_trips(&Action::Input { id: "field".to_string(), value: InputValue::Bool(true) });
round_trips(&Action::Restore { data: "{}".to_string() });
}
#[test]
fn widget_round_trips() {
round_trips(&Widget::Text { content: "hi".to_string(), style: TextStyle::Title });
round_trips(&Widget::ColorDot { color: ProjectColor::Teal });
round_trips(&Widget::Chart { values: vec![1.0, 2.5, 3.0], labels: vec!["a".to_string()], style: ChartStyle::Bar });
round_trips(&Widget::Calendar { year: 2026, month: 6, first_weekday: 1, selected: Some(15), on_day: vec!["d1".to_string(), "d2".to_string()] });
round_trips(&Widget::SwipeAction { child: Box::new(Widget::Divider), actions: vec![SwipeButton { label: "Del".to_string(), tone: Tone::Danger, on_tap: "t".to_string() }] });
round_trips(&Widget::Scaffold {
title: "T".to_string(),
body: Box::new(Widget::Divider),
tabs: vec![Tab { label: "A".to_string(), selected: true, on_select: "t".to_string(), icon: Some(Icon::Home) }],
back: Some("b".to_string()),
dark_mode: true,
theme: None,
fab: None,
sheet: None,
on_refresh: None,
refreshing: false,
route: "r".to_string(),
depth: 2,
});
round_trips(&Widget::Scaffold {
title: "T".to_string(),
body: Box::new(Widget::Divider),
tabs: vec![],
back: None,
dark_mode: false,
theme: Some(Theme {
seed: Rgb::new(0xC8, 0x5A, 0x3C),
accent: Some(Rgb::new(0xE0, 0x6A, 0x2C)),
corner: Corner::Large,
density: Density::Compact,
font: FontFamily::Rounded,
}),
fab: Some(Fab { icon: Icon::Calendar, on_press: "f".to_string() }),
sheet: Some(Sheet { title: "S".to_string(), child: Box::new(Widget::Divider), on_dismiss: "d".to_string() }),
on_refresh: Some("r".to_string()),
refreshing: true,
route: "r".to_string(),
depth: 1,
});
}
}