Skip to main content

yog_ui/component/
designer.rs

1//! The screen designer's protocol (see the [module docs](super)): one C-ABI
2//! entry point, `yog_ui_designer`, taking a [`Request`] and answering a
3//! [`Reply`] — binary Yog Data both — through a write callback.
4//!
5//! - [`Request::Describe`] → [`Answer::Described`]: the mod's components.
6//! - [`Request::Render`] → [`Answer::Rendered`]: builds the component,
7//!   sizes it `w × h` (≤ 0: its natural size, within `avail_w × avail_h`),
8//!   draws it at the origin through the `YogGfxApi`, and answers its size.
9//!
10//! A failure (unknown component, panic in `build`) answers the error.
11
12use std::ffi::c_void;
13use std::panic::{AssertUnwindSafe, catch_unwind};
14
15use yog_abi::{YogBytes, YogGfxApi};
16use yog_data::Schema;
17use yog_gfx::GfxContext;
18
19use super::{COMMON_ATTRS, PropKind, Props, apply_style, components, find};
20use crate::widget::{self, Widget};
21use crate::{FlexDir, UiRoot, Units};
22
23/// Version of this protocol; the designer host refuses others.
24pub const ABI: u32 = 2;
25
26/// `write(user_data, bytes)`: the host's sink for the reply.
27pub type WriteFn = unsafe extern "C" fn(user_data: *mut c_void, reply: YogBytes);
28
29/// What the designer host asks a mod.
30#[derive(Debug, Clone, PartialEq, Schema)]
31#[data(tag = "request")]
32pub enum Request {
33    Describe,
34    Render(Render),
35}
36
37/// A component to draw, at the origin.
38#[derive(Debug, Clone, PartialEq, Schema)]
39pub struct Render {
40    pub name: String,
41    /// Its attributes, as a `.yui` writes them.
42    #[data(default)]
43    pub attrs: Vec<(String, String)>,
44    /// Its size; ≤ 0: its natural size.
45    pub w: f32,
46    pub h: f32,
47    /// The room it has.
48    pub avail_w: f32,
49    pub avail_h: f32,
50}
51
52/// What a mod answers.
53#[derive(Debug, Clone, PartialEq, Schema)]
54#[data(tag = "answer")]
55pub enum Answer {
56    Described { components: Vec<ComponentInfo> },
57    Rendered { w: f32, h: f32 },
58}
59
60/// The answer, or what went wrong.
61pub type Reply = Result<Answer, String>;
62
63/// One component as the mod describes it.
64#[derive(Debug, Clone, PartialEq, Schema)]
65pub struct ComponentInfo {
66    pub name: String,
67    /// Toolbox tooltip and property grid header (may be `@Set.key`).
68    #[data(default)]
69    pub description: String,
70    /// The toolbox label (empty: `name`; may be `@Set.key`).
71    #[data(default)]
72    pub display_name: String,
73    /// The toolbox icon — a Yog image source or `@Icons.x` (empty: a glyph).
74    #[data(default)]
75    pub icon: String,
76    /// The toolbox category (empty: "<mod> Components"; may be `@Set.key`).
77    #[data(default)]
78    pub category: String,
79    /// The canvas size for editing it, and an instance's size when dropped (0: unset).
80    #[data(default)]
81    pub design_w: f32,
82    #[data(default)]
83    pub design_h: f32,
84    /// Whether the toolbox offers it.
85    #[data(default = "yes")]
86    pub toolbox: bool,
87    #[data(default)]
88    pub props: Vec<PropInfo>,
89    /// The `.yui` declaring it (the designer's own listing; never the mod's).
90    #[data(default)]
91    pub file: Option<String>,
92}
93
94/// One property of a component.
95#[derive(Debug, Clone, PartialEq, Schema)]
96pub struct PropInfo {
97    pub name: String,
98    /// `text`, `number`, `int`, `bool`, `color`, `choice`, `event`, `image`.
99    pub kind: String,
100    #[data(default)]
101    pub default: String,
102    #[data(default)]
103    pub doc: String,
104    /// The property grid category (empty: the component's own).
105    #[data(default)]
106    pub category: String,
107    #[data(default)]
108    pub options: Vec<String>,
109}
110
111fn yes() -> bool {
112    true
113}
114
115/// `export_mod!`'s `yog_ui_designer`.
116///
117/// # Safety
118/// `request` must point at its bytes for the call; `gfx` a valid table (or
119/// null for `describe`); `write` sound to call with `user_data`.
120pub unsafe fn call(
121    request: YogBytes,
122    gfx: *const YogGfxApi,
123    user_data: *mut c_void,
124    write: WriteFn,
125) -> bool {
126    // SAFETY: the caller's contract.
127    let request = unsafe { request.as_slice() };
128    let reply: Reply = match catch_unwind(AssertUnwindSafe(|| answer(request, gfx))) {
129        Ok(reply) => reply,
130        Err(panic) => Err(panic_message(&*panic)),
131    };
132    let bytes = yog_data::to_bytes(&reply);
133    // SAFETY: the caller's contract.
134    unsafe { write(user_data, YogBytes::new(&bytes)) };
135    reply.is_ok()
136}
137
138fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
139    let msg = panic
140        .downcast_ref::<&str>()
141        .map(|s| s.to_string())
142        .or_else(|| panic.downcast_ref::<String>().cloned())
143        .unwrap_or_else(|| "unknown panic".into());
144    format!("the component panicked: {msg}")
145}
146
147fn answer(request: &[u8], gfx: *const YogGfxApi) -> Reply {
148    let request = yog_data::read_bytes::<Request>(request)
149        .map_err(|e| format!("not a designer request: {e:?}"))?;
150    match request {
151        Request::Describe => Ok(describe()),
152        Request::Render(r) => {
153            if gfx.is_null() {
154                return Err("render: no gfx table".into());
155            }
156            // SAFETY: non-null, valid for the call (the caller's contract).
157            let ctx = unsafe { GfxContext::from_raw(gfx) };
158            render(&ctx, &r)
159        }
160    }
161}
162
163fn render(ctx: &GfxContext, r: &Render) -> Reply {
164    let spec = find(&r.name).ok_or_else(|| format!("no component named {}", r.name))?;
165    let (w, h, aw, ah) = (r.w, r.h, r.avail_w, r.avail_h);
166    type Attrs<'a> = Vec<(&'a str, &'a str)>;
167    let (common, own): (Attrs, Attrs) = r
168        .attrs
169        .iter()
170        .map(|(k, v)| (k.as_str(), v.as_str()))
171        .partition(|(k, _)| COMMON_ATTRS.contains(k));
172    let mut root: Widget = (spec.build)(&Props::new(spec.props, &own));
173    for (k, v) in common {
174        root = apply_style(root, k, v);
175    }
176    if w > 0.0 {
177        root = root.w(Units::Px(w));
178    }
179    if h > 0.0 {
180        root = root.h(Units::Px(h));
181    }
182    let (aw, ah) = (aw.max(1.0), ah.max(1.0));
183    let screen = widget::panel(FlexDir::Column)
184        .w(Units::Px(aw))
185        .h(Units::Px(ah))
186        .child(root.at(0.0, 0.0));
187    // Measured like the game measures: through the host's text/image metrics.
188    crate::text::use_host_metrics(ctx);
189    crate::image::use_host_images(ctx);
190    let mut ui = UiRoot::new(yog_ids::yog::DESIGNER_SCREEN, screen);
191    ui.layout(aw, ah);
192    let rect = ui
193        .layout_root
194        .children
195        .first()
196        .map(|n| n.rect)
197        .unwrap_or_default();
198    ui.render(ctx);
199    Ok(Answer::Rendered {
200        w: rect.w,
201        h: rect.h,
202    })
203}
204
205fn describe() -> Answer {
206    let components = components()
207        .iter()
208        .map(|c| ComponentInfo {
209            name: c.name.into(),
210            description: c.doc.into(),
211            display_name: c.display_name.into(),
212            icon: c.icon.into(),
213            category: c.category.into(),
214            design_w: c.design_size.0,
215            design_h: c.design_size.1,
216            toolbox: c.toolbox,
217            props: c
218                .props
219                .iter()
220                .map(|p| PropInfo {
221                    name: p.name.into(),
222                    kind: p.kind.as_str().into(),
223                    default: p.default.into(),
224                    doc: p.doc.into(),
225                    category: p.category.into(),
226                    options: match p.kind {
227                        PropKind::Choice(o) => o.iter().map(|s| s.to_string()).collect(),
228                        _ => Vec::new(),
229                    },
230                })
231                .collect(),
232            file: None,
233        })
234        .collect();
235    Answer::Described { components }
236}