1use std::collections::HashMap;
36use std::sync::{Mutex, PoisonError};
37
38use crate::widget::{self, Widget};
39use crate::FlexDir;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum PropKind {
44 Text,
46 Number,
48 Int,
50 Bool,
52 Color,
54 Choice(&'static [&'static str]),
56 Event,
59 Image,
61}
62
63impl PropKind {
64 pub fn as_str(self) -> &'static str {
66 match self {
67 PropKind::Text => "text",
68 PropKind::Number => "number",
69 PropKind::Int => "int",
70 PropKind::Bool => "bool",
71 PropKind::Color => "color",
72 PropKind::Choice(_) => "choice",
73 PropKind::Event => "event",
74 PropKind::Image => "image",
75 }
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub struct Prop {
82 pub name: &'static str,
83 pub kind: PropKind,
84 pub default: &'static str,
86 pub doc: &'static str,
88 pub category: &'static str,
90}
91
92impl Prop {
93 pub const fn new(name: &'static str, kind: PropKind, default: &'static str) -> Self {
94 Self { name, kind, default, doc: "", category: "" }
95 }
96 pub const fn text(name: &'static str, default: &'static str) -> Self {
97 Self::new(name, PropKind::Text, default)
98 }
99 pub const fn number(name: &'static str, default: &'static str) -> Self {
100 Self::new(name, PropKind::Number, default)
101 }
102 pub const fn int(name: &'static str, default: &'static str) -> Self {
103 Self::new(name, PropKind::Int, default)
104 }
105 pub const fn bool(name: &'static str, default: bool) -> Self {
106 Self::new(name, PropKind::Bool, if default { "true" } else { "false" })
107 }
108 pub const fn color(name: &'static str, default: &'static str) -> Self {
109 Self::new(name, PropKind::Color, default)
110 }
111 pub const fn choice(name: &'static str, options: &'static [&'static str], default: &'static str) -> Self {
112 Self::new(name, PropKind::Choice(options), default)
113 }
114 pub const fn event(name: &'static str) -> Self {
115 Self::new(name, PropKind::Event, "")
116 }
117 pub const fn image(name: &'static str, default: &'static str) -> Self {
118 Self::new(name, PropKind::Image, default)
119 }
120 pub const fn doc(self, doc: &'static str) -> Self {
122 Self { doc, ..self }
123 }
124 pub const fn category(self, category: &'static str) -> Self {
126 Self { category, ..self }
127 }
128}
129
130pub trait Component {
133 const NAME: &'static str;
135 const DOC: &'static str = "";
138 const DISPLAY_NAME: &'static str = "";
140 const ICON: &'static str = "";
143 const CATEGORY: &'static str = "";
145 const DESIGN_SIZE: (f32, f32) = (0.0, 0.0);
147 const TOOLBOX: bool = true;
149 const PROPS: &'static [Prop] = &[];
150 fn build(props: &Props) -> Widget;
153}
154
155#[derive(Debug, Clone, Copy)]
157pub struct ComponentSpec {
158 pub name: &'static str,
159 pub doc: &'static str,
160 pub display_name: &'static str,
161 pub icon: &'static str,
162 pub category: &'static str,
163 pub design_size: (f32, f32),
164 pub toolbox: bool,
165 pub props: &'static [Prop],
166 pub build: fn(&Props) -> Widget,
167}
168
169impl ComponentSpec {
170 pub fn of<C: Component>() -> Self {
171 Self {
172 name: C::NAME,
173 doc: C::DOC,
174 display_name: C::DISPLAY_NAME,
175 icon: C::ICON,
176 category: C::CATEGORY,
177 design_size: C::DESIGN_SIZE,
178 toolbox: C::TOOLBOX,
179 props: C::PROPS,
180 build: C::build,
181 }
182 }
183}
184
185static REGISTRY: Mutex<Vec<ComponentSpec>> = Mutex::new(Vec::new());
186
187pub fn register<C: Component>() {
191 register_spec(ComponentSpec::of::<C>());
192}
193
194pub fn register_spec(spec: ComponentSpec) {
195 let mut all = REGISTRY.lock().unwrap_or_else(PoisonError::into_inner);
196 all.retain(|s| s.name != spec.name);
197 all.push(spec);
198}
199
200pub fn components() -> Vec<ComponentSpec> {
202 REGISTRY.lock().unwrap_or_else(PoisonError::into_inner).clone()
203}
204
205pub fn find(name: &str) -> Option<ComponentSpec> {
207 REGISTRY.lock().unwrap_or_else(PoisonError::into_inner).iter().find(|s| s.name == name).copied()
208}
209
210pub fn build(name: &str, attrs: &[(&str, &str)]) -> Widget {
214 match find(name) {
215 Some(spec) => (spec.build)(&Props::new(spec.props, attrs)),
216 None => error_box(&format!("unknown component <{name}>")),
217 }
218}
219
220pub fn error_box(message: &str) -> Widget {
222 widget::panel(FlexDir::Column)
223 .bg(0xC0_400000)
224 .padding(2.0, 2.0, 2.0, 2.0)
225 .child(widget::label(message).color(0xFF_FF8080))
226 .min_w(16.0)
227 .min_h(12.0)
228}
229
230#[derive(Debug, Clone, Default)]
232pub struct Props {
233 values: HashMap<String, String>,
234}
235
236impl Props {
237 pub fn new(props: &[Prop], attrs: &[(&str, &str)]) -> Self {
239 let mut values: HashMap<String, String> = props.iter().map(|p| (p.name.to_string(), p.default.to_string())).collect();
240 for (k, v) in attrs {
241 values.insert((*k).to_string(), (*v).to_string());
242 }
243 Self { values }
244 }
245
246 pub fn get(&self, name: &str) -> Option<&str> {
248 self.values.get(name).map(String::as_str)
249 }
250
251 pub fn text(&self, name: &str) -> String {
253 let v = self.get(name).unwrap_or_default();
254 match v.strip_prefix("@@") {
255 Some(rest) => format!("@{rest}"),
256 None => v.to_string(),
257 }
258 }
259
260 pub fn number(&self, name: &str) -> f32 {
262 self.get(name).and_then(|v| v.trim().parse().ok()).unwrap_or(0.0)
263 }
264
265 pub fn int(&self, name: &str) -> i64 {
267 self.get(name).and_then(|v| v.trim().parse().ok()).unwrap_or(0)
268 }
269
270 pub fn bool(&self, name: &str) -> bool {
272 self.get(name).is_some_and(|v| v.trim() == "true")
273 }
274
275 pub fn color(&self, name: &str) -> u32 {
277 self.get(name).and_then(parse_color).unwrap_or(0xFF_FFFFFF)
278 }
279
280 pub fn word(&self, name: &str) -> &str {
282 self.get(name).map(str::trim).unwrap_or_default()
283 }
284}
285
286pub mod bind {
289 use std::fmt::Display;
290
291 use crate::Units;
292
293 pub fn text<T: Display + ?Sized>(v: &T) -> String {
295 v.to_string()
296 }
297
298 pub fn color<T: BindColor>(v: T) -> u32 {
300 v.to_argb()
301 }
302
303 pub fn flag(v: impl Into<bool>) -> bool {
305 v.into()
306 }
307
308 pub fn count<T: BindNumber>(v: T) -> u32 {
310 v.to_f32().max(0.0) as u32
311 }
312
313 pub fn number<T: BindNumber>(v: T) -> f32 {
315 v.to_f32()
316 }
317
318 pub fn units<T: BindUnits>(v: T) -> Units {
320 v.to_units()
321 }
322
323 pub trait BindColor {
324 fn to_argb(self) -> u32;
325 }
326 impl BindColor for u32 {
327 fn to_argb(self) -> u32 {
328 self
329 }
330 }
331 impl BindColor for &u32 {
332 fn to_argb(self) -> u32 {
333 *self
334 }
335 }
336 impl BindColor for &str {
337 fn to_argb(self) -> u32 {
338 super::parse_color(self).unwrap_or(0xFF_FFFFFF)
339 }
340 }
341 impl BindColor for &&str {
342 fn to_argb(self) -> u32 {
343 (*self).to_argb()
344 }
345 }
346 impl BindColor for &String {
347 fn to_argb(self) -> u32 {
348 self.as_str().to_argb()
349 }
350 }
351 impl BindColor for String {
352 fn to_argb(self) -> u32 {
353 self.as_str().to_argb()
354 }
355 }
356
357 pub trait BindNumber {
358 fn to_f32(self) -> f32;
359 }
360 macro_rules! numbers {
361 ($($t:ty),*) => {$(
362 impl BindNumber for $t {
363 fn to_f32(self) -> f32 {
364 self as f32
365 }
366 }
367 impl BindNumber for &$t {
368 fn to_f32(self) -> f32 {
369 *self as f32
370 }
371 }
372 )*};
373 }
374 numbers!(f32, f64, i32, i64, u32, u64, usize);
375
376 pub trait BindUnits {
377 fn to_units(self) -> Units;
378 }
379 impl<T: BindNumber> BindUnits for T {
380 fn to_units(self) -> Units {
381 Units::Px(self.to_f32())
382 }
383 }
384 impl BindUnits for Units {
385 fn to_units(self) -> Units {
386 self
387 }
388 }
389 impl BindUnits for &str {
390 fn to_units(self) -> Units {
391 let v = self.trim();
392 if v.is_empty() || v == "auto" {
393 Units::Auto
394 } else if let Some(p) = v.strip_suffix('%') {
395 Units::Percent(p.trim().parse().unwrap_or(0.0))
396 } else {
397 Units::Px(v.parse().unwrap_or(0.0))
398 }
399 }
400 }
401 impl BindUnits for &Units {
402 fn to_units(self) -> Units {
403 *self
404 }
405 }
406 impl BindUnits for &&str {
407 fn to_units(self) -> Units {
408 (*self).to_units()
409 }
410 }
411 impl BindUnits for &String {
412 fn to_units(self) -> Units {
413 self.as_str().to_units()
414 }
415 }
416}
417
418pub const COMMON_ATTRS: &[&str] = &[
422 "id", "x", "y", "w", "h", "min_w", "min_h", "flex", "dock", "gap", "padding", "bg", "color", "align", "font_scale",
423 "font", "shadow", "no_wrap", "ellipsis", "focus_style", "focus_color", "enabled", "on_click", "visible",
424];
425
426pub fn apply_style(w: Widget, key: &str, value: &str) -> Widget {
430 let v = value.trim();
431 let num = || v.parse::<f32>().ok();
432 match key {
433 "bg" => parse_color(v).map_or(w.clone(), |c| w.clone().bg(c)),
434 "color" => parse_color(v).map_or(w.clone(), |c| w.clone().color(c)),
435 "focus_color" => parse_color(v).map_or(w.clone(), |c| w.clone().focus_color(c)),
436 "min_w" => num().map_or(w.clone(), |n| w.clone().min_w(n)),
437 "min_h" => num().map_or(w.clone(), |n| w.clone().min_h(n)),
438 "gap" => num().map_or(w.clone(), |n| w.clone().gap(n)),
439 "font_scale" => num().map_or(w.clone(), |n| w.clone().font_scale(n)),
440 "font" => w.font(v),
441 "shadow" => w.shadow(v == "true"),
442 "enabled" => w.enabled(v != "false"),
443 "no_wrap" if v == "true" => w.no_wrap(),
444 "ellipsis" if v == "true" => w.ellipsis(),
445 "align" => w.align(match v {
446 "center" => crate::Align::Center,
447 "end" => crate::Align::End,
448 _ => crate::Align::Start,
449 }),
450 "padding" => {
451 let parts: Option<Vec<f32>> = v.split([' ', ',']).filter(|s| !s.is_empty()).map(|s| s.parse().ok()).collect();
452 match parts.as_deref() {
453 Some([a]) => w.padding(*a, *a, *a, *a),
454 Some([vv, hh]) => w.padding(*vv, *hh, *vv, *hh),
455 Some([t, r, b, l]) => w.padding(*t, *r, *b, *l),
456 _ => w,
457 }
458 }
459 _ => w,
460 }
461}
462
463pub fn parse_color(value: &str) -> Option<u32> {
465 let v = value.trim();
466 let hex = v.strip_prefix('#').or_else(|| v.strip_prefix("0x")).or_else(|| v.strip_prefix("0X"))?.replace('_', "");
467 let n = u32::from_str_radix(&hex, 16).ok()?;
468 match hex.len() {
469 6 => Some(0xFF00_0000 | n),
470 8 => Some(n),
471 _ => None,
472 }
473}
474
475pub mod designer {
490 use std::ffi::c_void;
491 use std::fmt::Write as _;
492 use std::panic::{catch_unwind, AssertUnwindSafe};
493
494 use yog_abi::{YogGfxApi, YogStr};
495 use yog_gfx::GfxContext;
496
497 use super::{apply_style, components, find, PropKind, Props, COMMON_ATTRS};
498 use crate::widget::{self, Widget};
499 use crate::{FlexDir, UiRoot, Units};
500
501 pub const ABI: u32 = 1;
503
504 pub type WriteFn = unsafe extern "C" fn(user_data: *mut c_void, text: YogStr);
506
507 pub unsafe fn call(request: YogStr, gfx: *const YogGfxApi, user_data: *mut c_void, write: WriteFn) -> bool {
513 let request = unsafe { request.as_str() };
515 let (ok, answer) = match catch_unwind(AssertUnwindSafe(|| answer(request, gfx))) {
516 Ok(Ok(a)) => (true, a),
517 Ok(Err(e)) => (false, e),
518 Err(panic) => (false, panic_message(&*panic)),
519 };
520 unsafe { write(user_data, YogStr::from_str(&answer)) };
522 ok
523 }
524
525 fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
526 let msg = panic
527 .downcast_ref::<&str>()
528 .map(|s| s.to_string())
529 .or_else(|| panic.downcast_ref::<String>().cloned())
530 .unwrap_or_else(|| "unknown panic".into());
531 format!("the component panicked: {msg}")
532 }
533
534 fn answer(request: &str, gfx: *const YogGfxApi) -> Result<String, String> {
535 let mut lines = request.lines();
536 match lines.next().map(str::trim) {
537 Some("describe") => Ok(describe()),
538 Some("render") => {
539 if gfx.is_null() {
540 return Err("render: no gfx table".into());
541 }
542 let name = lines.next().unwrap_or_default().trim().to_string();
543 let size: Vec<f32> = lines.next().unwrap_or_default().split_whitespace().filter_map(|n| n.parse().ok()).collect();
544 let [w, h, aw, ah] = size[..] else { return Err("render: expected `w h avail_w avail_h`".into()) };
545 let attrs: Vec<(String, String)> =
546 lines.filter_map(|l| l.split_once('=')).map(|(k, v)| (k.trim().to_string(), unescape(v))).collect();
547 let ctx = unsafe { GfxContext::from_raw(gfx) };
549 render(&ctx, &name, &attrs, [w, h, aw, ah])
550 }
551 other => Err(format!("unknown designer request {other:?}")),
552 }
553 }
554
555 fn render(ctx: &GfxContext, name: &str, attrs: &[(String, String)], [w, h, aw, ah]: [f32; 4]) -> Result<String, String> {
556 let spec = find(name).ok_or_else(|| format!("no component named {name}"))?;
557 type Attrs<'a> = Vec<(&'a str, &'a str)>;
558 let (common, own): (Attrs, Attrs) =
559 attrs.iter().map(|(k, v)| (k.as_str(), v.as_str())).partition(|(k, _)| COMMON_ATTRS.contains(k));
560 let mut root: Widget = (spec.build)(&Props::new(spec.props, &own));
561 for (k, v) in common {
562 root = apply_style(root, k, v);
563 }
564 if w > 0.0 {
565 root = root.w(Units::Px(w));
566 }
567 if h > 0.0 {
568 root = root.h(Units::Px(h));
569 }
570 let (aw, ah) = (aw.max(1.0), ah.max(1.0));
571 let screen = widget::panel(FlexDir::Column).w(Units::Px(aw)).h(Units::Px(ah)).child(root.at(0.0, 0.0));
572 crate::text::use_host_metrics(ctx);
574 crate::image::use_host_images(ctx);
575 let mut ui = UiRoot::new("yog:designer", screen);
576 ui.layout(aw, ah);
577 let rect = ui.layout_root.children.first().map(|n| n.rect).unwrap_or_default();
578 ui.render(ctx);
579 Ok(format!("{{\"w\":{},\"h\":{}}}", rect.w, rect.h))
580 }
581
582 fn describe() -> String {
583 let mut out = format!("{{\"abi\":{ABI},\"components\":[");
584 for (i, c) in components().iter().enumerate() {
585 if i > 0 {
586 out.push(',');
587 }
588 let _ = write!(
589 out,
590 "{{\"name\":{},\"description\":{},\"display_name\":{},\"icon\":{},\"category\":{},\"design_w\":{},\"design_h\":{},\"toolbox\":{},\"props\":[",
591 json(c.name),
592 json(c.doc),
593 json(c.display_name),
594 json(c.icon),
595 json(c.category),
596 c.design_size.0,
597 c.design_size.1,
598 c.toolbox
599 );
600 for (j, p) in c.props.iter().enumerate() {
601 if j > 0 {
602 out.push(',');
603 }
604 let options = match p.kind {
605 PropKind::Choice(o) => o.iter().map(|s| json(s)).collect::<Vec<_>>().join(","),
606 _ => String::new(),
607 };
608 let _ = write!(
609 out,
610 "{{\"name\":{},\"kind\":{},\"default\":{},\"doc\":{},\"category\":{},\"options\":[{options}]}}",
611 json(p.name),
612 json(p.kind.as_str()),
613 json(p.default),
614 json(p.doc),
615 json(p.category)
616 );
617 }
618 out.push_str("]}");
619 }
620 out.push_str("]}");
621 out
622 }
623
624 fn json(s: &str) -> String {
626 let mut out = String::with_capacity(s.len() + 2);
627 out.push('"');
628 for c in s.chars() {
629 match c {
630 '"' => out.push_str("\\\""),
631 '\\' => out.push_str("\\\\"),
632 '\n' => out.push_str("\\n"),
633 '\r' => out.push_str("\\r"),
634 '\t' => out.push_str("\\t"),
635 c if (c as u32) < 0x20 => {
636 let _ = write!(out, "\\u{:04x}", c as u32);
637 }
638 c => out.push(c),
639 }
640 }
641 out.push('"');
642 out
643 }
644
645 pub fn unescape(v: &str) -> String {
647 let mut out = String::with_capacity(v.len());
648 let mut chars = v.chars();
649 while let Some(c) = chars.next() {
650 if c != '\\' {
651 out.push(c);
652 continue;
653 }
654 match chars.next() {
655 Some('n') => out.push('\n'),
656 Some(other) => out.push(other),
657 None => out.push('\\'),
658 }
659 }
660 out
661 }
662
663 pub fn escape(v: &str) -> String {
665 v.replace('\\', "\\\\").replace('\n', "\\n")
666 }
667}
668
669#[cfg(test)]
670mod tests {
671 use super::*;
672
673 struct Badge;
674 impl Component for Badge {
675 const NAME: &'static str = "TestBadge";
676 const ICON: &'static str = "item:apple";
677 const DESIGN_SIZE: (f32, f32) = (40.0, 12.0);
678 const PROPS: &'static [Prop] = &[
679 Prop::text("label", "new"),
680 Prop::color("fill", "#FF3060C0").doc("Background"),
681 Prop::choice("size", &["small", "large"], "small"),
682 ];
683 fn build(p: &Props) -> Widget {
684 widget::label(p.text("label")).bg(p.color("fill")).padding(1.0, 2.0, 1.0, 2.0)
685 }
686 }
687
688 #[test]
689 fn props_fall_back_to_defaults() {
690 let p = Props::new(Badge::PROPS, &[("label", "@@x"), ("n", " 2.5 ")]);
691 assert_eq!(p.text("label"), "@x");
692 assert_eq!(p.color("fill"), 0xFF3060C0);
693 assert_eq!(p.word("size"), "small");
694 assert_eq!(p.number("n"), 2.5);
695 assert_eq!(p.int("missing"), 0);
696 assert!(!p.bool("missing"));
697 }
698
699 #[test]
700 fn builds_registered_components() {
701 register::<Badge>();
702 let w = build("TestBadge", &[("label", "hi")]);
703 assert!(matches!(&w.kind, crate::widget::WidgetKind::Label(t) if t == "hi"));
704 assert_eq!(w.style.bg, 0xFF3060C0);
705 let missing = build("Nope", &[]);
706 assert!(!missing.children.is_empty());
707 }
708
709 #[test]
710 fn describes_and_escapes() {
711 register::<Badge>();
712 let mut out = String::new();
713 unsafe extern "C" fn sink(ud: *mut std::ffi::c_void, s: yog_abi::YogStr) {
714 unsafe { (*(ud as *mut String)).push_str(s.as_str()) };
716 }
717 let req = "describe";
718 let ok = unsafe { designer::call(yog_abi::YogStr::from_str(req), std::ptr::null(), &mut out as *mut String as *mut _, sink) };
719 assert!(ok, "{out}");
720 assert!(out.contains("\"name\":\"TestBadge\""), "{out}");
721 assert!(out.contains("\"options\":[\"small\",\"large\"]"), "{out}");
722 assert!(out.contains("\"icon\":\"item:apple\",\"category\":\"\",\"design_w\":40,\"design_h\":12,\"toolbox\":true"), "{out}");
723 assert_eq!(designer::unescape(&designer::escape("a\\b\nc")), "a\\b\nc");
724 }
725}