use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use denise::icon::Icon;
use denise::{Radius, Role};
use super::{Align, Fit, Orientation, avatar::Presence};
pub const ROLES: &[&str] = &[
"base-100",
"base-200",
"base-300",
"base-content",
"primary",
"primary-content",
"secondary",
"secondary-content",
"accent",
"accent-content",
"neutral",
"neutral-content",
"info",
"info-content",
"success",
"success-content",
"warning",
"warning-content",
"error",
"error-content",
];
pub const RADII: &[&str] = &["selector", "field", "box"];
pub const ALIGNMENTS: &[&str] = &["start", "center", "end"];
pub const ORIENTATIONS: &[&str] = &["horizontal", "vertical"];
pub const FITS: &[&str] = &["fill", "contain", "cover", "center"];
pub const PRESENCES: &[&str] = &["online", "offline", "busy"];
pub const SIDES: &[&str] = &["above", "below", "before", "after"];
pub const fn role_from_name(name: &str) -> Option<Role> {
let bytes = name.as_bytes();
let mut i = 0;
while i < ROLES.len() {
if const_eq(ROLES[i].as_bytes(), bytes) {
return Some(ROLE_VALUES[i]);
}
i += 1;
}
None
}
const ROLE_VALUES: [Role; 20] = [
Role::Base100,
Role::Base200,
Role::Base300,
Role::BaseContent,
Role::Primary,
Role::PrimaryContent,
Role::Secondary,
Role::SecondaryContent,
Role::Accent,
Role::AccentContent,
Role::Neutral,
Role::NeutralContent,
Role::Info,
Role::InfoContent,
Role::Success,
Role::SuccessContent,
Role::Warning,
Role::WarningContent,
Role::Error,
Role::ErrorContent,
];
const fn const_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut i = 0;
while i < a.len() {
if a[i] != b[i] {
return false;
}
i += 1;
}
true
}
pub const fn role_name(role: Role) -> &'static str {
ROLES[role as usize]
}
pub const fn radius_name(radius: Radius) -> &'static str {
match radius {
Radius::Selector => "selector",
Radius::Field => "field",
Radius::Box => "box",
}
}
pub fn radius_from_name(name: &str) -> Option<Radius> {
Some(match name {
"selector" => Radius::Selector,
"field" => Radius::Field,
"box" => Radius::Box,
_ => return None,
})
}
pub const fn align_name(align: Align) -> &'static str {
match align {
Align::Start => "start",
Align::Center => "center",
Align::End => "end",
}
}
pub fn align_from_name(name: &str) -> Option<Align> {
Some(match name {
"start" => Align::Start,
"center" => Align::Center,
"end" => Align::End,
_ => return None,
})
}
pub const fn orientation_name(orientation: Orientation) -> &'static str {
match orientation {
Orientation::Horizontal => "horizontal",
Orientation::Vertical => "vertical",
}
}
pub fn orientation_from_name(name: &str) -> Option<Orientation> {
Some(match name {
"horizontal" => Orientation::Horizontal,
"vertical" => Orientation::Vertical,
_ => return None,
})
}
pub const fn fit_name(fit: Fit) -> &'static str {
match fit {
Fit::Fill => "fill",
Fit::Contain => "contain",
Fit::Cover => "cover",
Fit::Center => "center",
}
}
pub fn fit_from_name(name: &str) -> Option<Fit> {
Some(match name {
"fill" => Fit::Fill,
"contain" => Fit::Contain,
"cover" => Fit::Cover,
"center" => Fit::Center,
_ => return None,
})
}
pub const fn presence_name(presence: Presence) -> &'static str {
match presence {
Presence::Online => "online",
Presence::Offline => "offline",
Presence::Busy => "busy",
}
}
pub fn presence_from_name(name: &str) -> Option<Presence> {
Some(match name {
"online" => Presence::Online,
"offline" => Presence::Offline,
"busy" => Presence::Busy,
_ => return None,
})
}
pub const fn side_name(side: crate::Side) -> &'static str {
use crate::Side;
match side {
Side::Above => "above",
Side::Below => "below",
Side::Before => "before",
Side::After => "after",
}
}
pub fn side_from_name(name: &str) -> Option<crate::Side> {
use crate::Side;
Some(match name {
"above" => Side::Above,
"below" => Side::Below,
"before" => Side::Before,
"after" => Side::After,
_ => return None,
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Payload {
None,
Bool,
Index,
Number,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum PropertyKind {
Text,
Bool,
Int {
min: i32,
max: i32,
},
Float {
min: f32,
max: f32,
},
Enum(&'static [&'static str]),
Message(Payload),
Asset,
Color,
List,
Placeholder,
}
impl PropertyKind {
pub const fn is_collection(self) -> bool {
matches!(self, PropertyKind::List | PropertyKind::Placeholder)
}
pub const fn noun(self) -> &'static str {
match self {
PropertyKind::Text => "a string",
PropertyKind::Bool => "true or false",
PropertyKind::Int { .. } => "a whole number",
PropertyKind::Float { .. } => "a number",
PropertyKind::Enum(_) => "one of the listed names",
PropertyKind::Message(_) => "a message name",
PropertyKind::Asset => "a path",
PropertyKind::Color => "a colour like #RRGGBB",
PropertyKind::List => "a run of child nodes",
PropertyKind::Placeholder => "a run of child nodes in a `design` block",
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Property {
pub name: &'static str,
pub kind: PropertyKind,
pub doc: &'static str,
pub pixels: bool,
}
impl Property {
pub const fn new(name: &'static str, kind: PropertyKind, doc: &'static str) -> Self {
Self {
name,
kind,
doc,
pixels: false,
}
}
#[must_use]
pub const fn in_pixels(mut self) -> Self {
self.pixels = true;
self
}
pub const fn is_settable(&self) -> bool {
!matches!(
self.kind,
PropertyKind::Message(_)
| PropertyKind::Asset
| PropertyKind::List
| PropertyKind::Placeholder
)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum Value {
Text(String),
Bool(bool),
Int(i32),
Float(f32),
Enum(&'static str),
}
impl Value {
pub fn text(text: impl Into<String>) -> Self {
Value::Text(text.into())
}
pub const fn role(role: Role) -> Self {
Value::Enum(role_name(role))
}
pub fn as_text(self) -> Result<String, Mismatch> {
match self {
Value::Text(text) => Ok(text),
_ => Err(Mismatch::wrong(PropertyKind::Text)),
}
}
pub fn as_bool(self) -> Result<bool, Mismatch> {
match self {
Value::Bool(value) => Ok(value),
_ => Err(Mismatch::wrong(PropertyKind::Bool)),
}
}
pub fn as_int(self) -> Result<i32, Mismatch> {
match self {
Value::Int(value) => Ok(value),
_ => Err(Mismatch::wrong(PropertyKind::Int {
min: i32::MIN,
max: i32::MAX,
})),
}
}
pub fn as_float(self) -> Result<f32, Mismatch> {
match self {
Value::Float(value) => Ok(value),
Value::Int(value) => Ok(value as f32),
_ => Err(Mismatch::wrong(PropertyKind::Float {
min: f32::MIN,
max: f32::MAX,
})),
}
}
pub fn as_name(self) -> Result<&'static str, Mismatch> {
match self {
Value::Enum(name) => Ok(name),
_ => Err(Mismatch::wrong(PropertyKind::Enum(&[]))),
}
}
pub fn as_size(self) -> Result<u16, Mismatch> {
Ok(self.as_int()?.clamp(1, u16::MAX as i32) as u16)
}
pub fn as_count(self) -> Result<u32, Mismatch> {
Ok(self.as_int()?.max(0) as u32)
}
pub fn as_millis(self) -> Result<u64, Mismatch> {
Ok(self.as_int()?.max(0) as u64)
}
pub fn as_index(self) -> Result<usize, Mismatch> {
Ok(self.as_int()?.max(0) as usize)
}
pub const fn align(align: Align) -> Self {
Value::Enum(align_name(align))
}
pub const fn radius(radius: Radius) -> Self {
Value::Enum(radius_name(radius))
}
pub const fn orientation(orientation: Orientation) -> Self {
Value::Enum(orientation_name(orientation))
}
pub const fn fit(fit: Fit) -> Self {
Value::Enum(fit_name(fit))
}
pub const fn presence(presence: Presence) -> Self {
Value::Enum(presence_name(presence))
}
pub fn as_role(self) -> Result<Role, Mismatch> {
role_from_name(self.as_name()?).ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(ROLES)))
}
pub fn as_align(self) -> Result<Align, Mismatch> {
align_from_name(self.as_name()?)
.ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(ALIGNMENTS)))
}
pub fn as_radius(self) -> Result<Radius, Mismatch> {
radius_from_name(self.as_name()?).ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(RADII)))
}
pub fn as_orientation(self) -> Result<Orientation, Mismatch> {
orientation_from_name(self.as_name()?)
.ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(ORIENTATIONS)))
}
pub fn as_fit(self) -> Result<Fit, Mismatch> {
fit_from_name(self.as_name()?).ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(FITS)))
}
pub fn as_presence(self) -> Result<Presence, Mismatch> {
presence_from_name(self.as_name()?)
.ok_or_else(|| Mismatch::wrong(PropertyKind::Enum(PRESENCES)))
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Mismatch {
Unknown,
WrongType {
expected: PropertyKind,
},
Supplied,
}
impl Mismatch {
const fn wrong(expected: PropertyKind) -> Self {
Mismatch::WrongType { expected }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct PropertyError {
pub kind: &'static str,
pub name: String,
pub mismatch: Mismatch,
pub accepted: &'static [Property],
}
impl fmt::Display for PropertyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.mismatch {
Mismatch::Unknown => {
write!(f, "`{}` has no property `{}`", self.kind, self.name)?;
if !self.accepted.is_empty() {
let names: Vec<&str> = self.accepted.iter().map(|p| p.name).collect();
write!(f, "; it accepts {}", names.join(", "))?;
}
Ok(())
}
Mismatch::WrongType { expected } => write!(
f,
"`{}` on `{}` takes {}",
self.name,
self.kind,
expected.noun()
),
Mismatch::Supplied => write!(
f,
"`{}` on `{}` is supplied when the widget is built, not set afterwards",
self.name, self.kind
),
}
}
}
impl core::error::Error for PropertyError {}
pub trait Describe {
const KIND: &'static str;
const DOC: &'static str;
const GROUP: Group;
const ICON: &'static Icon;
const PROPERTIES: &'static [Property];
fn get(&self, name: &str) -> Option<Value>;
fn apply(&mut self, name: &str, value: Value) -> Result<(), Mismatch>;
fn set(&mut self, name: &str, value: Value) -> Result<(), PropertyError> {
self.apply(name, value).map_err(|mismatch| PropertyError {
kind: Self::KIND,
name: name.to_string(),
mismatch,
accepted: Self::PROPERTIES,
})
}
}
pub trait DynDescribe {
fn kind(&self) -> &'static str;
fn properties(&self) -> &'static [Property];
fn get_property(&self, name: &str) -> Option<Value>;
fn set_property(&mut self, name: &str, value: Value) -> Result<(), PropertyError>;
}
impl<T: Describe> DynDescribe for T {
fn kind(&self) -> &'static str {
T::KIND
}
fn properties(&self) -> &'static [Property] {
T::PROPERTIES
}
fn get_property(&self, name: &str) -> Option<Value> {
self.get(name)
}
fn set_property(&mut self, name: &str, value: Value) -> Result<(), PropertyError> {
self.set(name, value)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct WidgetInfo {
pub kind: &'static str,
pub doc: &'static str,
pub group: Group,
pub icon: &'static Icon,
pub properties: &'static [Property],
}
impl WidgetInfo {
pub const fn of<W: Describe>() -> Self {
Self {
kind: W::KIND,
doc: W::DOC,
group: W::GROUP,
icon: W::ICON,
properties: W::PROPERTIES,
}
}
pub fn property(&self, name: &str) -> Option<&'static Property> {
self.properties.iter().find(|p| p.name == name)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum Group {
Input,
Display,
Indicator,
Container,
Data,
Media,
}
impl Group {
pub const ALL: [Self; 6] = [
Self::Input,
Self::Display,
Self::Indicator,
Self::Container,
Self::Data,
Self::Media,
];
pub const fn name(self) -> &'static str {
match self {
Self::Input => "input",
Self::Display => "display",
Self::Indicator => "indicator",
Self::Container => "container",
Self::Data => "data",
Self::Media => "media",
}
}
}
pub fn all() -> &'static [WidgetInfo] {
ALL
}
static ALL: &[WidgetInfo] = &[
WidgetInfo::of::<super::Alert>(),
WidgetInfo::of::<super::Avatar>(),
WidgetInfo::of::<super::Badge>(),
WidgetInfo::of::<super::Button<crate::Void>>(),
WidgetInfo::of::<super::Carousel<crate::Void>>(),
WidgetInfo::of::<super::Checkbox<crate::Void>>(),
WidgetInfo::of::<super::Collapse<crate::Void>>(),
WidgetInfo::of::<super::Divider>(),
WidgetInfo::of::<super::Image>(),
WidgetInfo::of::<super::Label>(),
WidgetInfo::of::<super::List<crate::Void>>(),
WidgetInfo::of::<super::MenuBar<crate::Void>>(),
WidgetInfo::of::<super::Panel>(),
WidgetInfo::of::<super::Progress>(),
WidgetInfo::of::<super::RadialProgress>(),
WidgetInfo::of::<super::RadioGroup<crate::Void>>(),
WidgetInfo::of::<super::Rating<crate::Void>>(),
WidgetInfo::of::<super::Select<crate::Void>>(),
WidgetInfo::of::<super::Slider<crate::Void>>(),
WidgetInfo::of::<super::Spinner>(),
WidgetInfo::of::<super::Table<crate::Void>>(),
WidgetInfo::of::<super::Tabs<crate::Void>>(),
WidgetInfo::of::<super::TextInput<crate::Void>>(),
WidgetInfo::of::<super::Timeline>(),
WidgetInfo::of::<super::Toggle<crate::Void>>(),
WidgetInfo::of::<super::Tree<crate::Void>>(),
WidgetInfo::of::<super::Video>(),
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_role_round_trips_through_its_name() {
for (index, name) in ROLES.iter().enumerate() {
let role = role_from_name(name).expect("a name in the table names a role");
assert_eq!(role as usize, index, "{name} is out of order");
assert_eq!(role_name(role), *name);
}
}
#[test]
fn a_name_outside_the_table_is_not_a_role() {
assert_eq!(role_from_name("puce"), None);
assert_eq!(role_from_name(""), None);
assert_eq!(role_from_name("primary-"), None);
}
#[test]
fn the_catalogue_names_are_unique_and_sorted() {
let mut names: Vec<&str> = all().iter().map(|w| w.kind).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "two widgets share a kind");
}
#[test]
fn no_widget_declares_the_same_property_twice() {
for widget in all() {
let mut names: Vec<&str> = widget.properties.iter().map(|p| p.name).collect();
let count = names.len();
names.sort_unstable();
names.dedup();
assert_eq!(names.len(), count, "{} repeats a property", widget.kind);
}
}
#[test]
fn every_property_is_kebab_case_and_documented() {
for widget in all() {
for property in widget.properties {
assert!(
!property.name.is_empty()
&& property
.name
.bytes()
.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-'),
"{}.{} is not kebab-case",
widget.kind,
property.name
);
assert!(
!property.doc.is_empty(),
"{}.{} has no documentation",
widget.kind,
property.name
);
}
}
}
#[test]
fn every_widget_says_in_one_line_what_it_is() {
for widget in all() {
let doc = widget.doc;
assert!(!doc.is_empty(), "{} says nothing about itself", widget.kind);
assert!(
!doc.contains('\n'),
"{}'s line is more than one",
widget.kind
);
assert!(
doc.starts_with(|c: char| c.is_uppercase()),
"{}: `{doc}` does not start a sentence",
widget.kind,
);
assert!(
doc.ends_with('.'),
"{}: `{doc}` does not end one",
widget.kind
);
assert!(
(20..=100).contains(&doc.len()),
"{}: `{doc}` is {} characters",
widget.kind,
doc.len(),
);
}
}
#[test]
fn no_two_widgets_describe_themselves_the_same_way() {
let mut docs: Vec<&str> = all().iter().map(|w| w.doc).collect();
let count = docs.len();
docs.sort_unstable();
docs.dedup();
assert_eq!(docs.len(), count, "two widgets say the same thing");
}
#[test]
fn every_group_has_something_on_it() {
for group in Group::ALL {
assert!(
all().iter().any(|w| w.group == group),
"nothing is `{}`",
group.name(),
);
}
for widget in all() {
assert!(
Group::ALL.contains(&widget.group),
"{} is in a group `Group::ALL` does not list",
widget.kind,
);
}
}
#[test]
fn an_enum_property_offers_names_it_would_accept() {
for widget in all() {
for property in widget.properties {
if let PropertyKind::Enum(names) = property.kind {
assert!(
!names.is_empty(),
"{}.{} offers no names",
widget.kind,
property.name
);
}
}
}
}
}