1use std::collections::HashMap;
36use yog_sync::Mutex;
37
38use yog_gfx::Color;
39use yog_ids::{yui_attr as attr, yui_value as value};
40
41use crate::FlexDir;
42use crate::widget::{self, Widget};
43use yog_ids::prop_kind;
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum PropKind {
48 Text,
50 Number,
52 Int,
54 Bool,
56 Color,
58 Choice(&'static [&'static str]),
60 Event,
63 Image,
65}
66
67impl PropKind {
68 pub fn as_str(self) -> &'static str {
70 match self {
71 PropKind::Text => prop_kind::TEXT,
72 PropKind::Number => prop_kind::NUMBER,
73 PropKind::Int => prop_kind::INT,
74 PropKind::Bool => prop_kind::BOOL,
75 PropKind::Color => prop_kind::COLOR,
76 PropKind::Choice(_) => prop_kind::CHOICE,
77 PropKind::Event => prop_kind::EVENT,
78 PropKind::Image => prop_kind::IMAGE,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Prop {
86 pub name: &'static str,
87 pub kind: PropKind,
88 pub default: &'static str,
90 pub doc: &'static str,
92 pub category: &'static str,
94}
95
96impl Prop {
97 pub const fn new(name: &'static str, kind: PropKind, default: &'static str) -> Self {
98 Self {
99 name,
100 kind,
101 default,
102 doc: "",
103 category: "",
104 }
105 }
106 pub const fn text(name: &'static str, default: &'static str) -> Self {
107 Self::new(name, PropKind::Text, default)
108 }
109 pub const fn number(name: &'static str, default: &'static str) -> Self {
110 Self::new(name, PropKind::Number, default)
111 }
112 pub const fn int(name: &'static str, default: &'static str) -> Self {
113 Self::new(name, PropKind::Int, default)
114 }
115 pub const fn bool(name: &'static str, default: bool) -> Self {
116 Self::new(
117 name,
118 PropKind::Bool,
119 if default { value::TRUE } else { value::FALSE },
120 )
121 }
122 pub const fn color(name: &'static str, default: &'static str) -> Self {
123 Self::new(name, PropKind::Color, default)
124 }
125 pub const fn choice(
126 name: &'static str,
127 options: &'static [&'static str],
128 default: &'static str,
129 ) -> Self {
130 Self::new(name, PropKind::Choice(options), default)
131 }
132 pub const fn event(name: &'static str) -> Self {
133 Self::new(name, PropKind::Event, "")
134 }
135 pub const fn image(name: &'static str, default: &'static str) -> Self {
136 Self::new(name, PropKind::Image, default)
137 }
138 pub const fn doc(self, doc: &'static str) -> Self {
140 Self { doc, ..self }
141 }
142 pub const fn category(self, category: &'static str) -> Self {
144 Self { category, ..self }
145 }
146}
147
148pub trait Component {
151 const NAME: &'static str;
153 const DOC: &'static str = "";
156 const DISPLAY_NAME: &'static str = "";
158 const ICON: &'static str = "";
161 const CATEGORY: &'static str = "";
163 const DESIGN_SIZE: (f32, f32) = (0.0, 0.0);
165 const TOOLBOX: bool = true;
167 const PROPS: &'static [Prop] = &[];
168 fn build(props: &Props) -> Widget;
171}
172
173#[derive(Debug, Clone, Copy)]
175pub struct ComponentSpec {
176 pub name: &'static str,
177 pub doc: &'static str,
178 pub display_name: &'static str,
179 pub icon: &'static str,
180 pub category: &'static str,
181 pub design_size: (f32, f32),
182 pub toolbox: bool,
183 pub props: &'static [Prop],
184 pub build: fn(&Props) -> Widget,
185}
186
187impl ComponentSpec {
188 pub fn of<C: Component>() -> Self {
189 Self {
190 name: C::NAME,
191 doc: C::DOC,
192 display_name: C::DISPLAY_NAME,
193 icon: C::ICON,
194 category: C::CATEGORY,
195 design_size: C::DESIGN_SIZE,
196 toolbox: C::TOOLBOX,
197 props: C::PROPS,
198 build: C::build,
199 }
200 }
201}
202
203static REGISTRY: Mutex<Vec<ComponentSpec>> = Mutex::new(Vec::new());
204
205pub fn register<C: Component>() {
209 register_spec(ComponentSpec::of::<C>());
210}
211
212pub fn register_spec(spec: ComponentSpec) {
213 let mut all = REGISTRY.lock();
214 all.retain(|s| s.name != spec.name);
215 all.push(spec);
216}
217
218pub fn components() -> Vec<ComponentSpec> {
220 REGISTRY.lock().clone()
221}
222
223pub fn find(name: &str) -> Option<ComponentSpec> {
225 REGISTRY.lock().iter().find(|s| s.name == name).copied()
226}
227
228pub fn build(name: &str, attrs: &[(&str, &str)]) -> Widget {
232 match find(name) {
233 Some(spec) => (spec.build)(&Props::new(spec.props, attrs)),
234 None => error_box(&format!("unknown component <{name}>")),
235 }
236}
237
238const ERROR_BG: Color = Color(0xC0_400000);
241const ERROR_TEXT: Color = Color(0xFF_FF8080);
242const ERROR_PAD: f32 = 2.0;
243const ERROR_MIN: [f32; 2] = [16.0, 12.0];
244
245pub fn error_box(message: &str) -> Widget {
247 widget::panel(FlexDir::Column)
248 .bg(ERROR_BG)
249 .padding(ERROR_PAD, ERROR_PAD, ERROR_PAD, ERROR_PAD)
250 .child(widget::label(message).color(ERROR_TEXT))
251 .min_w(ERROR_MIN[0])
252 .min_h(ERROR_MIN[1])
253}
254
255const FALLBACK_COLOR: Color = Color::WHITE;
257
258#[derive(Debug, Clone, Default)]
260pub struct Props {
261 values: HashMap<String, String>,
262}
263
264impl Props {
265 pub fn new(props: &[Prop], attrs: &[(&str, &str)]) -> Self {
267 let mut values: HashMap<String, String> = props
268 .iter()
269 .map(|p| (p.name.to_string(), p.default.to_string()))
270 .collect();
271 for (k, v) in attrs {
272 values.insert((*k).to_string(), (*v).to_string());
273 }
274 Self { values }
275 }
276
277 pub fn get(&self, name: &str) -> Option<&str> {
279 self.values.get(name).map(String::as_str)
280 }
281
282 pub fn text(&self, name: &str) -> String {
284 let v = self.get(name).unwrap_or_default();
285 match v.strip_prefix("@@") {
286 Some(rest) => format!("@{rest}"),
287 None => v.to_string(),
288 }
289 }
290
291 pub fn number(&self, name: &str) -> f32 {
293 self.get(name)
294 .and_then(|v| v.trim().parse().ok())
295 .unwrap_or(0.0)
296 }
297
298 pub fn int(&self, name: &str) -> i64 {
300 self.get(name)
301 .and_then(|v| v.trim().parse().ok())
302 .unwrap_or(0)
303 }
304
305 pub fn bool(&self, name: &str) -> bool {
307 self.get(name).is_some_and(|v| v.trim() == value::TRUE)
308 }
309
310 pub fn color(&self, name: &str) -> Color {
312 self.get(name)
313 .and_then(parse_color)
314 .unwrap_or(FALLBACK_COLOR)
315 }
316
317 pub fn word(&self, name: &str) -> &str {
319 self.get(name).map(str::trim).unwrap_or_default()
320 }
321}
322
323pub mod bind {
326 use std::fmt::Display;
327
328 use yog_gfx::Color;
329
330 use crate::Units;
331
332 pub fn text<T: Display + ?Sized>(v: &T) -> String {
334 v.to_string()
335 }
336
337 pub fn color<T: BindColor>(v: T) -> Color {
339 v.to_color()
340 }
341
342 pub fn flag(v: impl Into<bool>) -> bool {
344 v.into()
345 }
346
347 pub fn count<T: BindNumber>(v: T) -> u32 {
349 v.to_f32().max(0.0) as u32
350 }
351
352 pub fn number<T: BindNumber>(v: T) -> f32 {
354 v.to_f32()
355 }
356
357 pub fn units<T: BindUnits>(v: T) -> Units {
359 v.to_units()
360 }
361
362 pub trait BindColor {
363 fn to_color(self) -> Color;
364 }
365 impl BindColor for Color {
366 fn to_color(self) -> Color {
367 self
368 }
369 }
370 impl BindColor for &Color {
371 fn to_color(self) -> Color {
372 *self
373 }
374 }
375 impl BindColor for u32 {
376 fn to_color(self) -> Color {
377 Color(self)
378 }
379 }
380 impl BindColor for &u32 {
381 fn to_color(self) -> Color {
382 Color(*self)
383 }
384 }
385 impl BindColor for &str {
386 fn to_color(self) -> Color {
387 super::parse_color(self).unwrap_or(super::FALLBACK_COLOR)
388 }
389 }
390 impl BindColor for &&str {
391 fn to_color(self) -> Color {
392 (*self).to_color()
393 }
394 }
395 impl BindColor for &String {
396 fn to_color(self) -> Color {
397 self.as_str().to_color()
398 }
399 }
400 impl BindColor for String {
401 fn to_color(self) -> Color {
402 self.as_str().to_color()
403 }
404 }
405
406 pub trait BindNumber {
407 fn to_f32(self) -> f32;
408 }
409 macro_rules! numbers {
410 ($($t:ty),*) => {$(
411 impl BindNumber for $t {
412 fn to_f32(self) -> f32 {
413 self as f32
414 }
415 }
416 impl BindNumber for &$t {
417 fn to_f32(self) -> f32 {
418 *self as f32
419 }
420 }
421 )*};
422 }
423 numbers!(f32, f64, i32, i64, u32, u64, usize);
424
425 pub trait BindUnits {
426 fn to_units(self) -> Units;
427 }
428 impl<T: BindNumber> BindUnits for T {
429 fn to_units(self) -> Units {
430 Units::Px(self.to_f32())
431 }
432 }
433 impl BindUnits for Units {
434 fn to_units(self) -> Units {
435 self
436 }
437 }
438 impl BindUnits for &str {
439 fn to_units(self) -> Units {
440 let v = self.trim();
441 if v.is_empty() || v == yog_ids::yui_value::AUTO {
442 Units::Auto
443 } else if let Some(p) = v.strip_suffix('%') {
444 Units::Percent(p.trim().parse().unwrap_or(0.0))
445 } else {
446 Units::Px(v.parse().unwrap_or(0.0))
447 }
448 }
449 }
450 impl BindUnits for &Units {
451 fn to_units(self) -> Units {
452 *self
453 }
454 }
455 impl BindUnits for &&str {
456 fn to_units(self) -> Units {
457 (*self).to_units()
458 }
459 }
460 impl BindUnits for &String {
461 fn to_units(self) -> Units {
462 self.as_str().to_units()
463 }
464 }
465}
466
467pub const COMMON_ATTRS: &[&str] = &[
471 attr::ID,
472 attr::X,
473 attr::Y,
474 attr::W,
475 attr::H,
476 attr::MIN_W,
477 attr::MIN_H,
478 attr::FLEX,
479 attr::DOCK,
480 attr::GAP,
481 attr::PADDING,
482 attr::BG,
483 attr::COLOR,
484 attr::ALIGN,
485 attr::FONT_SCALE,
486 attr::FONT,
487 attr::SHADOW,
488 attr::NO_WRAP,
489 attr::ELLIPSIS,
490 attr::FOCUS_STYLE,
491 attr::FOCUS_COLOR,
492 attr::ENABLED,
493 attr::ON_CLICK,
494 attr::VISIBLE,
495];
496
497pub fn apply_style(w: Widget, key: &str, value: &str) -> Widget {
501 match crate::attr::Attr::parse(key, value) {
502 Ok(Some(attr)) if attr.is_style() => attr.apply(w),
503 _ => match key {
504 attr::FONT => w.font(value.trim()),
505 _ => w,
506 },
507 }
508}
509
510pub fn parse_color(value: &str) -> Option<Color> {
513 Color::parse(value)
514}
515
516pub mod designer;
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521
522 struct Badge;
523 impl Component for Badge {
524 const NAME: &'static str = "TestBadge";
525 const ICON: &'static str = "item:apple";
526 const DESIGN_SIZE: (f32, f32) = (40.0, 12.0);
527 const PROPS: &'static [Prop] = &[
528 Prop::text("label", "new"),
529 Prop::color("fill", "#FF3060C0").doc("Background"),
530 Prop::choice("size", &["small", "large"], "small"),
531 ];
532 fn build(p: &Props) -> Widget {
533 widget::label(p.text("label"))
534 .bg(p.color("fill"))
535 .padding(1.0, 2.0, 1.0, 2.0)
536 }
537 }
538
539 #[test]
540 fn props_fall_back_to_defaults() {
541 let p = Props::new(Badge::PROPS, &[("label", "@@x"), ("n", " 2.5 ")]);
542 assert_eq!(p.text("label"), "@x");
543 assert_eq!(p.color("fill"), Color(0xFF3060C0));
544 assert_eq!(p.word("size"), "small");
545 assert_eq!(p.number("n"), 2.5);
546 assert_eq!(p.int("missing"), 0);
547 assert!(!p.bool("missing"));
548 }
549
550 #[test]
551 fn builds_registered_components() {
552 register::<Badge>();
553 let w = build("TestBadge", &[("label", "hi")]);
554 assert!(matches!(&w.kind, crate::widget::WidgetKind::Label(t) if t == "hi"));
555 assert_eq!(w.style.bg, Color(0xFF3060C0));
556 let missing = build("Nope", &[]);
557 assert!(!missing.children.is_empty());
558 }
559
560 #[test]
561 fn describes_its_components() {
562 register::<Badge>();
563 let mut out = Vec::new();
564 unsafe extern "C" fn sink(ud: *mut std::ffi::c_void, b: yog_abi::YogBytes) {
565 unsafe { (*(ud as *mut Vec<u8>)).extend_from_slice(b.as_slice()) };
567 }
568 let req = yog_data::to_bytes(&designer::Request::Describe);
569 let ok = unsafe {
570 designer::call(
571 yog_abi::YogBytes::new(&req),
572 std::ptr::null(),
573 &mut out as *mut Vec<u8> as *mut _,
574 sink,
575 )
576 };
577 let reply: designer::Reply = yog_data::read_bytes(&out).unwrap();
578 assert!(ok, "{reply:?}");
579 let Ok(designer::Answer::Described { components }) = reply else {
580 panic!("{reply:?}");
581 };
582 let badge = components.iter().find(|c| c.name == "TestBadge").unwrap();
583 assert_eq!(
584 (badge.icon.as_str(), badge.design_w, badge.design_h),
585 ("item:apple", 40.0, 12.0)
586 );
587 assert!(badge.toolbox && badge.category.is_empty());
588 assert!(badge.props.iter().any(|p| p.options == ["small", "large"]));
589 }
590}