1use std::ffi::c_void;
20use std::io::{BufRead, Write};
21use std::path::{Path, PathBuf};
22
23use serde::{Deserialize, Serialize};
24use serde_json::{Value, json};
25use yog_abi::{YogGfxApi, YogStr};
26
27use super::raster::DrawOp;
28use super::{gfx, images, raster};
29
30pub const MARK: &str = "\u{1}yog-designer\u{1}";
32
33pub const DESIGNER_ABI: u32 = 1;
35
36type DesignerFn = unsafe extern "C" fn(
37 YogStr,
38 *const YogGfxApi,
39 *mut c_void,
40 unsafe extern "C" fn(*mut c_void, YogStr),
41) -> bool;
42
43pub struct Loaded {
45 _lib: libloading::Library,
46 call: DesignerFn,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51pub struct ComponentInfo {
52 pub name: String,
53 #[serde(default)]
55 pub description: String,
56 #[serde(default)]
58 pub display_name: String,
59 #[serde(default)]
61 pub icon: String,
62 #[serde(default)]
64 pub category: String,
65 #[serde(default)]
67 pub design_w: f32,
68 #[serde(default)]
69 pub design_h: f32,
70 #[serde(default = "yes")]
72 pub toolbox: bool,
73 #[serde(default)]
74 pub props: Vec<PropInfo>,
75 #[serde(default, skip_serializing_if = "Option::is_none")]
77 pub file: Option<String>,
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
81pub struct PropInfo {
82 pub name: String,
83 pub kind: String,
85 #[serde(default)]
86 pub default: String,
87 #[serde(default)]
88 pub doc: String,
89 #[serde(default)]
91 pub category: String,
92 #[serde(default)]
93 pub options: Vec<String>,
94}
95
96fn yes() -> bool {
97 true
98}
99
100unsafe extern "C" fn sink(user_data: *mut c_void, text: YogStr) {
101 unsafe { (*(user_data as *mut String)).push_str(text.as_str()) };
103}
104
105impl Loaded {
106 pub fn open(lib: &Path) -> Result<Self, String> {
108 let lib = unsafe { libloading::Library::new(lib) }
111 .map_err(|e| format!("can't load {}: {e}", lib.display()))?;
112 unsafe {
114 let abi = lib
115 .get::<unsafe extern "C" fn() -> u32>(b"yog_abi_version\0")
116 .map_err(|_| {
117 "not a Yog mod library (no yog_abi_version — is `export_mod!` missing?)"
118 .to_string()
119 })?;
120 let abi = abi();
121 if abi != yog_abi::ABI_VERSION {
122 return Err(format!(
123 "the mod is built against Yog ABI {abi}, the designer against {} — update yog-api (or Yog-IDLE) so they match",
124 yog_abi::ABI_VERSION
125 ));
126 }
127 let designer = lib
128 .get::<unsafe extern "C" fn() -> u32>(b"yog_ui_designer_abi\0")
129 .map_err(|_| {
130 "the mod's yog-api has no designer support (yog_ui_designer) — update yog-api"
131 .to_string()
132 })?;
133 let designer = designer();
134 if designer != DESIGNER_ABI {
135 return Err(format!(
136 "designer protocol {designer} (this IDE speaks {DESIGNER_ABI}) — update yog-api or Yog-IDLE"
137 ));
138 }
139 let call = *lib
140 .get::<DesignerFn>(b"yog_ui_designer\0")
141 .map_err(|e| e.to_string())?;
142 Ok(Self { _lib: lib, call })
143 }
144 }
145
146 fn call(&self, request: &str, gfx: *const YogGfxApi) -> Result<String, String> {
147 let mut out = String::new();
148 let ok = unsafe {
150 (self.call)(
151 YogStr::from_str(request),
152 gfx,
153 &mut out as *mut String as *mut c_void,
154 sink,
155 )
156 };
157 if ok { Ok(out) } else { Err(out) }
158 }
159
160 pub fn describe(&self) -> Result<Vec<ComponentInfo>, String> {
161 let answer = self.call("describe", std::ptr::null())?;
162 #[derive(Deserialize)]
163 struct Described {
164 components: Vec<ComponentInfo>,
165 }
166 serde_json::from_str::<Described>(&answer)
167 .map(|d| d.components)
168 .map_err(|e| format!("bad describe answer: {e}"))
169 }
170
171 pub fn render(&self, req: &RenderReq) -> Result<(f32, f32, Vec<DrawOp>), String> {
173 let mut request = format!(
174 "render\n{}\n{} {} {} {}\n",
175 req.name, req.w, req.h, req.avail_w, req.avail_h
176 );
177 for (k, v) in &req.props {
178 request.push_str(k);
179 request.push('=');
180 request.push_str(&v.replace('\\', "\\\\").replace('\n', "\\n"));
181 request.push('\n');
182 }
183 let (aw, ah) = (req.avail_w.max(1.0) as i32, req.avail_h.max(1.0) as i32);
184 let mut answer = Err(String::new());
185 let table = gfx::api(aw, ah, 1.0);
187 let ops = gfx::record(aw, ah, |_| answer = self.call(&request, &table));
188 let answer = answer?;
189 let size: Value =
190 serde_json::from_str(&answer).map_err(|e| format!("bad render answer: {e}"))?;
191 let dim = |k: &str| size.get(k).and_then(Value::as_f64).unwrap_or(0.0) as f32;
192 Ok((dim("w"), dim("h"), ops))
193 }
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
197#[serde(rename_all = "camelCase")]
198pub struct RenderReq {
199 pub name: String,
200 #[serde(default)]
201 pub props: Vec<(String, String)>,
202 #[serde(default)]
203 pub w: f32,
204 #[serde(default)]
205 pub h: f32,
206 #[serde(default = "big")]
207 pub avail_w: f32,
208 #[serde(default = "big")]
209 pub avail_h: f32,
210}
211
212const UNBOUNDED: f32 = 1024.0;
214
215fn big() -> f32 {
216 UNBOUNDED
217}
218
219#[derive(Deserialize)]
220#[serde(tag = "cmd", rename_all = "camelCase")]
221enum Request {
222 #[serde(rename_all = "camelCase")]
223 Load {
224 lib: PathBuf,
225 mod_root: Option<PathBuf>,
226 },
227 Render(RenderReq),
228 Png {
229 #[serde(flatten)]
230 req: RenderReq,
231 #[serde(default)]
232 scale: Option<u32>,
233 out: PathBuf,
234 },
235}
236
237struct Host {
238 lib: Option<Loaded>,
239 mod_root: Option<PathBuf>,
240}
241
242impl Host {
243 fn handle(&mut self, line: &str) -> Value {
244 let request: Request = match serde_json::from_str(line) {
245 Ok(r) => r,
246 Err(e) => return json!({"ok": false, "error": format!("bad request: {e}")}),
247 };
248 let result = match request {
249 Request::Load { lib, mod_root } => {
250 if self.lib.is_some() {
251 return json!({"ok": false, "error": "a library is already loaded (restart the host to reload)"});
252 }
253 images::set_document(mod_root.as_deref());
254 self.mod_root = mod_root;
255 Loaded::open(&lib).and_then(|l| {
256 let components = l.describe()?;
257 self.lib = Some(l);
258 Ok(json!({"ok": true, "components": components}))
259 })
260 }
261 Request::Render(req) => self
262 .loaded()
263 .and_then(|l| l.render(&req))
264 .map(|(w, h, ops)| json!({"ok": true, "w": w, "h": h, "ops": ops})),
265 Request::Png { req, scale, out } => self
266 .loaded()
267 .and_then(|l| l.render(&req))
268 .and_then(|(w, h, ops)| {
269 let res = super::assets::Resources::for_mod(self.mod_root.as_deref());
270 let (cw, ch) = (w.ceil().max(1.0) as u32, h.ceil().max(1.0) as u32);
271 let canvas =
272 raster::rasterize(&ops, cw, ch, scale.unwrap_or(4).clamp(1, 8), &res);
273 std::fs::write(&out, canvas.png()?)
274 .map_err(|e| format!("{}: {e}", out.display()))?;
275 Ok(json!({"ok": true, "w": w, "h": h, "ops": ops.len(), "out": out}))
276 }),
277 };
278 result.unwrap_or_else(|e| json!({"ok": false, "error": e}))
279 }
280
281 fn loaded(&self) -> Result<&Loaded, String> {
282 self.lib
283 .as_ref()
284 .ok_or_else(|| "no library loaded".to_string())
285 }
286}
287
288pub const COMMAND: &str = "designer-host";
290
291pub fn serve() {
293 let mut host = Host {
294 lib: None,
295 mod_root: None,
296 };
297 let stdin = std::io::stdin();
298 for line in stdin.lock().lines() {
299 let Ok(line) = line else { break };
300 if line.trim().is_empty() {
301 continue;
302 }
303 let answer = host.handle(&line);
304 let mut out = std::io::stdout().lock();
305 if writeln!(out, "{MARK}{answer}")
306 .and_then(|_| out.flush())
307 .is_err()
308 {
309 break;
310 }
311 }
312}
313
314#[cfg(test)]
315mod tests {
316 use super::*;
317
318 #[test]
319 fn requests_parse() {
320 let r: Request = serde_json::from_str(
321 r#"{"cmd":"render","name":"StatBar","props":[["value","0.5"]],"w":0}"#,
322 )
323 .unwrap();
324 assert!(
325 matches!(r, Request::Render(RenderReq { ref name, ref props, avail_w, .. }) if name == "StatBar" && props.len() == 1 && avail_w == 1024.0)
326 );
327 let mut host = Host {
328 lib: None,
329 mod_root: None,
330 };
331 assert_eq!(
332 host.handle(r#"{"cmd":"render","name":"X"}"#)["error"],
333 "no library loaded"
334 );
335 assert!(
336 host.handle(r#"{"cmd":"load","lib":"/nowhere/libnope.so"}"#)["error"]
337 .as_str()
338 .unwrap()
339 .contains("can't load")
340 );
341 }
342
343 #[test]
346 #[ignore]
347 fn renders_a_built_mod() {
348 let (Ok(lib), Ok(name)) = (
349 std::env::var("YOG_DESIGNER_LIB"),
350 std::env::var("YOG_DESIGNER_COMPONENT"),
351 ) else {
352 return;
353 };
354 let props = std::env::var("YOG_DESIGNER_PROPS").unwrap_or_default();
355 let props = props
356 .split(';')
357 .filter_map(|kv| kv.split_once('='))
358 .map(|(k, v)| (k.to_string(), v.to_string()))
359 .collect();
360 let mut host = Host {
361 lib: None,
362 mod_root: None,
363 };
364 let loaded = host.handle(&json!({"cmd": "load", "lib": lib}).to_string());
365 eprintln!("{loaded:#}");
366 let l = host.loaded().unwrap();
367 let (w, h, ops) = l
368 .render(&RenderReq {
369 name: name.clone(),
370 props,
371 w: 0.0,
372 h: 0.0,
373 avail_w: 400.0,
374 avail_h: 300.0,
375 })
376 .unwrap();
377 eprintln!("{w}x{h}: {ops:?}");
378 assert!(w > 0.0 && h > 0.0 && !ops.is_empty());
379 if let Ok(out) = std::env::var("YOG_DESIGNER_PNG") {
380 let res = super::super::assets::Resources::for_mod(None);
381 let canvas = raster::rasterize(&ops, w.ceil() as u32, h.ceil() as u32, 6, &res);
382 std::fs::write(&out, canvas.png().unwrap()).unwrap();
383 }
384 }
385}