use std::collections::HashMap;
use yog_sync::Mutex;
use yog_gfx::Color;
use yog_ids::{yui_attr as attr, yui_value as value};
use crate::FlexDir;
use crate::widget::{self, Widget};
use yog_ids::prop_kind;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PropKind {
Text,
Number,
Int,
Bool,
Color,
Choice(&'static [&'static str]),
Event,
Image,
}
impl PropKind {
pub fn as_str(self) -> &'static str {
match self {
PropKind::Text => prop_kind::TEXT,
PropKind::Number => prop_kind::NUMBER,
PropKind::Int => prop_kind::INT,
PropKind::Bool => prop_kind::BOOL,
PropKind::Color => prop_kind::COLOR,
PropKind::Choice(_) => prop_kind::CHOICE,
PropKind::Event => prop_kind::EVENT,
PropKind::Image => prop_kind::IMAGE,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Prop {
pub name: &'static str,
pub kind: PropKind,
pub default: &'static str,
pub doc: &'static str,
pub category: &'static str,
}
impl Prop {
pub const fn new(name: &'static str, kind: PropKind, default: &'static str) -> Self {
Self {
name,
kind,
default,
doc: "",
category: "",
}
}
pub const fn text(name: &'static str, default: &'static str) -> Self {
Self::new(name, PropKind::Text, default)
}
pub const fn number(name: &'static str, default: &'static str) -> Self {
Self::new(name, PropKind::Number, default)
}
pub const fn int(name: &'static str, default: &'static str) -> Self {
Self::new(name, PropKind::Int, default)
}
pub const fn bool(name: &'static str, default: bool) -> Self {
Self::new(
name,
PropKind::Bool,
if default { value::TRUE } else { value::FALSE },
)
}
pub const fn color(name: &'static str, default: &'static str) -> Self {
Self::new(name, PropKind::Color, default)
}
pub const fn choice(
name: &'static str,
options: &'static [&'static str],
default: &'static str,
) -> Self {
Self::new(name, PropKind::Choice(options), default)
}
pub const fn event(name: &'static str) -> Self {
Self::new(name, PropKind::Event, "")
}
pub const fn image(name: &'static str, default: &'static str) -> Self {
Self::new(name, PropKind::Image, default)
}
pub const fn doc(self, doc: &'static str) -> Self {
Self { doc, ..self }
}
pub const fn category(self, category: &'static str) -> Self {
Self { category, ..self }
}
}
pub trait Component {
const NAME: &'static str;
const DOC: &'static str = "";
const DISPLAY_NAME: &'static str = "";
const ICON: &'static str = "";
const CATEGORY: &'static str = "";
const DESIGN_SIZE: (f32, f32) = (0.0, 0.0);
const TOOLBOX: bool = true;
const PROPS: &'static [Prop] = &[];
fn build(props: &Props) -> Widget;
}
#[derive(Debug, Clone, Copy)]
pub struct ComponentSpec {
pub name: &'static str,
pub doc: &'static str,
pub display_name: &'static str,
pub icon: &'static str,
pub category: &'static str,
pub design_size: (f32, f32),
pub toolbox: bool,
pub props: &'static [Prop],
pub build: fn(&Props) -> Widget,
}
impl ComponentSpec {
pub fn of<C: Component>() -> Self {
Self {
name: C::NAME,
doc: C::DOC,
display_name: C::DISPLAY_NAME,
icon: C::ICON,
category: C::CATEGORY,
design_size: C::DESIGN_SIZE,
toolbox: C::TOOLBOX,
props: C::PROPS,
build: C::build,
}
}
}
static REGISTRY: Mutex<Vec<ComponentSpec>> = Mutex::new(Vec::new());
pub fn register<C: Component>() {
register_spec(ComponentSpec::of::<C>());
}
pub fn register_spec(spec: ComponentSpec) {
let mut all = REGISTRY.lock();
all.retain(|s| s.name != spec.name);
all.push(spec);
}
pub fn components() -> Vec<ComponentSpec> {
REGISTRY.lock().clone()
}
pub fn find(name: &str) -> Option<ComponentSpec> {
REGISTRY.lock().iter().find(|s| s.name == name).copied()
}
pub fn build(name: &str, attrs: &[(&str, &str)]) -> Widget {
match find(name) {
Some(spec) => (spec.build)(&Props::new(spec.props, attrs)),
None => error_box(&format!("unknown component <{name}>")),
}
}
const ERROR_BG: Color = Color(0xC0_400000);
const ERROR_TEXT: Color = Color(0xFF_FF8080);
const ERROR_PAD: f32 = 2.0;
const ERROR_MIN: [f32; 2] = [16.0, 12.0];
pub fn error_box(message: &str) -> Widget {
widget::panel(FlexDir::Column)
.bg(ERROR_BG)
.padding(ERROR_PAD, ERROR_PAD, ERROR_PAD, ERROR_PAD)
.child(widget::label(message).color(ERROR_TEXT))
.min_w(ERROR_MIN[0])
.min_h(ERROR_MIN[1])
}
const FALLBACK_COLOR: Color = Color::WHITE;
#[derive(Debug, Clone, Default)]
pub struct Props {
values: HashMap<String, String>,
}
impl Props {
pub fn new(props: &[Prop], attrs: &[(&str, &str)]) -> Self {
let mut values: HashMap<String, String> = props
.iter()
.map(|p| (p.name.to_string(), p.default.to_string()))
.collect();
for (k, v) in attrs {
values.insert((*k).to_string(), (*v).to_string());
}
Self { values }
}
pub fn get(&self, name: &str) -> Option<&str> {
self.values.get(name).map(String::as_str)
}
pub fn text(&self, name: &str) -> String {
let v = self.get(name).unwrap_or_default();
match v.strip_prefix("@@") {
Some(rest) => format!("@{rest}"),
None => v.to_string(),
}
}
pub fn number(&self, name: &str) -> f32 {
self.get(name)
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0.0)
}
pub fn int(&self, name: &str) -> i64 {
self.get(name)
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0)
}
pub fn bool(&self, name: &str) -> bool {
self.get(name).is_some_and(|v| v.trim() == value::TRUE)
}
pub fn color(&self, name: &str) -> Color {
self.get(name)
.and_then(parse_color)
.unwrap_or(FALLBACK_COLOR)
}
pub fn word(&self, name: &str) -> &str {
self.get(name).map(str::trim).unwrap_or_default()
}
}
pub mod bind {
use std::fmt::Display;
use yog_gfx::Color;
use crate::Units;
pub fn text<T: Display + ?Sized>(v: &T) -> String {
v.to_string()
}
pub fn color<T: BindColor>(v: T) -> Color {
v.to_color()
}
pub fn flag(v: impl Into<bool>) -> bool {
v.into()
}
pub fn count<T: BindNumber>(v: T) -> u32 {
v.to_f32().max(0.0) as u32
}
pub fn number<T: BindNumber>(v: T) -> f32 {
v.to_f32()
}
pub fn units<T: BindUnits>(v: T) -> Units {
v.to_units()
}
pub trait BindColor {
fn to_color(self) -> Color;
}
impl BindColor for Color {
fn to_color(self) -> Color {
self
}
}
impl BindColor for &Color {
fn to_color(self) -> Color {
*self
}
}
impl BindColor for u32 {
fn to_color(self) -> Color {
Color(self)
}
}
impl BindColor for &u32 {
fn to_color(self) -> Color {
Color(*self)
}
}
impl BindColor for &str {
fn to_color(self) -> Color {
super::parse_color(self).unwrap_or(super::FALLBACK_COLOR)
}
}
impl BindColor for &&str {
fn to_color(self) -> Color {
(*self).to_color()
}
}
impl BindColor for &String {
fn to_color(self) -> Color {
self.as_str().to_color()
}
}
impl BindColor for String {
fn to_color(self) -> Color {
self.as_str().to_color()
}
}
pub trait BindNumber {
fn to_f32(self) -> f32;
}
macro_rules! numbers {
($($t:ty),*) => {$(
impl BindNumber for $t {
fn to_f32(self) -> f32 {
self as f32
}
}
impl BindNumber for &$t {
fn to_f32(self) -> f32 {
*self as f32
}
}
)*};
}
numbers!(f32, f64, i32, i64, u32, u64, usize);
pub trait BindUnits {
fn to_units(self) -> Units;
}
impl<T: BindNumber> BindUnits for T {
fn to_units(self) -> Units {
Units::Px(self.to_f32())
}
}
impl BindUnits for Units {
fn to_units(self) -> Units {
self
}
}
impl BindUnits for &str {
fn to_units(self) -> Units {
let v = self.trim();
if v.is_empty() || v == yog_ids::yui_value::AUTO {
Units::Auto
} else if let Some(p) = v.strip_suffix('%') {
Units::Percent(p.trim().parse().unwrap_or(0.0))
} else {
Units::Px(v.parse().unwrap_or(0.0))
}
}
}
impl BindUnits for &Units {
fn to_units(self) -> Units {
*self
}
}
impl BindUnits for &&str {
fn to_units(self) -> Units {
(*self).to_units()
}
}
impl BindUnits for &String {
fn to_units(self) -> Units {
self.as_str().to_units()
}
}
}
pub const COMMON_ATTRS: &[&str] = &[
attr::ID,
attr::X,
attr::Y,
attr::W,
attr::H,
attr::MIN_W,
attr::MIN_H,
attr::FLEX,
attr::DOCK,
attr::GAP,
attr::PADDING,
attr::BG,
attr::COLOR,
attr::ALIGN,
attr::FONT_SCALE,
attr::FONT,
attr::SHADOW,
attr::NO_WRAP,
attr::ELLIPSIS,
attr::FOCUS_STYLE,
attr::FOCUS_COLOR,
attr::ENABLED,
attr::ON_CLICK,
attr::VISIBLE,
];
pub fn apply_style(w: Widget, key: &str, value: &str) -> Widget {
match crate::attr::Attr::parse(key, value) {
Ok(Some(attr)) if attr.is_style() => attr.apply(w),
_ => match key {
attr::FONT => w.font(value.trim()),
_ => w,
},
}
}
pub fn parse_color(value: &str) -> Option<Color> {
Color::parse(value)
}
pub mod designer;
#[cfg(test)]
mod tests {
use super::*;
struct Badge;
impl Component for Badge {
const NAME: &'static str = "TestBadge";
const ICON: &'static str = "item:apple";
const DESIGN_SIZE: (f32, f32) = (40.0, 12.0);
const PROPS: &'static [Prop] = &[
Prop::text("label", "new"),
Prop::color("fill", "#FF3060C0").doc("Background"),
Prop::choice("size", &["small", "large"], "small"),
];
fn build(p: &Props) -> Widget {
widget::label(p.text("label"))
.bg(p.color("fill"))
.padding(1.0, 2.0, 1.0, 2.0)
}
}
#[test]
fn props_fall_back_to_defaults() {
let p = Props::new(Badge::PROPS, &[("label", "@@x"), ("n", " 2.5 ")]);
assert_eq!(p.text("label"), "@x");
assert_eq!(p.color("fill"), Color(0xFF3060C0));
assert_eq!(p.word("size"), "small");
assert_eq!(p.number("n"), 2.5);
assert_eq!(p.int("missing"), 0);
assert!(!p.bool("missing"));
}
#[test]
fn builds_registered_components() {
register::<Badge>();
let w = build("TestBadge", &[("label", "hi")]);
assert!(matches!(&w.kind, crate::widget::WidgetKind::Label(t) if t == "hi"));
assert_eq!(w.style.bg, Color(0xFF3060C0));
let missing = build("Nope", &[]);
assert!(!missing.children.is_empty());
}
#[test]
fn describes_its_components() {
register::<Badge>();
let mut out = Vec::new();
unsafe extern "C" fn sink(ud: *mut std::ffi::c_void, b: yog_abi::YogBytes) {
unsafe { (*(ud as *mut Vec<u8>)).extend_from_slice(b.as_slice()) };
}
let req = yog_data::to_bytes(&designer::Request::Describe);
let ok = unsafe {
designer::call(
yog_abi::YogBytes::new(&req),
std::ptr::null(),
&mut out as *mut Vec<u8> as *mut _,
sink,
)
};
let reply: designer::Reply = yog_data::read_bytes(&out).unwrap();
assert!(ok, "{reply:?}");
let Ok(designer::Answer::Described { components }) = reply else {
panic!("{reply:?}");
};
let badge = components.iter().find(|c| c.name == "TestBadge").unwrap();
assert_eq!(
(badge.icon.as_str(), badge.design_w, badge.design_h),
("item:apple", 40.0, 12.0)
);
assert!(badge.toolbox && badge.category.is_empty());
assert!(badge.props.iter().any(|p| p.options == ["small", "large"]));
}
}