1use std::ffi::c_void;
17use std::io::{self, BufRead, Write};
18use std::path::{Path, PathBuf};
19
20use yog_abi::{YogBytes, YogGfxApi};
21use yog_data::Schema;
22use yog_ui::component::designer::{self as protocol, Answer};
23
24pub use yog_ui::component::designer::{ComponentInfo, PropInfo, Render as RenderReq};
25
26use super::raster::DrawOp;
27use super::{gfx, images, raster};
28
29pub const MARK: &[u8] = b"\x01yog-designer\x01";
31
32pub const DESIGNER_ABI: u32 = protocol::ABI;
34
35const PNG_SCALE: u32 = 4;
37const MAX_PNG_SCALE: u32 = 8;
38
39type DesignerFn =
40 unsafe extern "C" fn(YogBytes, *const YogGfxApi, *mut c_void, protocol::WriteFn) -> bool;
41
42pub struct Loaded {
44 _lib: libloading::Library,
45 call: DesignerFn,
46}
47
48unsafe extern "C" fn sink(user_data: *mut c_void, reply: YogBytes) {
49 unsafe { (*(user_data as *mut Vec<u8>)).extend_from_slice(reply.as_slice()) };
51}
52
53impl Loaded {
54 pub fn open(lib: &Path) -> Result<Self, String> {
56 let lib = unsafe { libloading::Library::new(lib) }
59 .map_err(|e| format!("can't load {}: {e}", lib.display()))?;
60 unsafe {
62 let abi = lib
63 .get::<unsafe extern "C" fn() -> u32>(b"yog_abi_version\0")
64 .map_err(|_| {
65 "not a Yog mod library (no yog_abi_version — is `export_mod!` missing?)"
66 .to_string()
67 })?;
68 let abi = abi();
69 if abi != yog_abi::ABI_VERSION {
70 return Err(format!(
71 "the mod is built against Yog ABI {abi}, the designer against {} — update yog-api (or Yog-IDLE) so they match",
72 yog_abi::ABI_VERSION
73 ));
74 }
75 let designer = lib
76 .get::<unsafe extern "C" fn() -> u32>(b"yog_ui_designer_abi\0")
77 .map_err(|_| {
78 "the mod's yog-api has no designer support (yog_ui_designer) — update yog-api"
79 .to_string()
80 })?;
81 let designer = designer();
82 if designer != DESIGNER_ABI {
83 return Err(format!(
84 "designer protocol {designer} (this IDE speaks {DESIGNER_ABI}) — update yog-api or Yog-IDLE"
85 ));
86 }
87 let call = *lib
88 .get::<DesignerFn>(b"yog_ui_designer\0")
89 .map_err(|e| e.to_string())?;
90 Ok(Self { _lib: lib, call })
91 }
92 }
93
94 fn ask(&self, request: &protocol::Request, gfx: *const YogGfxApi) -> Result<Answer, String> {
96 let request = yog_data::to_bytes(request);
97 let mut reply = Vec::new();
98 unsafe {
100 (self.call)(
101 YogBytes::new(&request),
102 gfx,
103 &mut reply as *mut Vec<u8> as *mut c_void,
104 sink,
105 )
106 };
107 yog_data::read_bytes::<protocol::Reply>(&reply)
108 .map_err(|e| format!("a mod's answer that isn't one: {e:?}"))?
109 }
110
111 pub fn describe(&self) -> Result<Vec<ComponentInfo>, String> {
112 match self.ask(&protocol::Request::Describe, std::ptr::null())? {
113 Answer::Described { components } => Ok(components),
114 other => Err(format!("asked to describe, the mod answered {other:?}")),
115 }
116 }
117
118 pub fn render(&self, req: &RenderReq) -> Result<(f32, f32, Vec<DrawOp>), String> {
120 let (aw, ah) = (req.avail_w.max(1.0) as i32, req.avail_h.max(1.0) as i32);
121 let mut answer = Err(String::new());
122 let table = gfx::api(aw, ah, 1.0);
124 let request = protocol::Request::Render(req.clone());
125 let ops = gfx::record(aw, ah, |_| answer = self.ask(&request, &table));
126 match answer? {
127 Answer::Rendered { w, h } => Ok((w, h, ops)),
128 other => Err(format!("asked to render, the mod answered {other:?}")),
129 }
130 }
131}
132
133#[derive(Debug, Clone, Schema)]
135#[data(tag = "cmd")]
136pub enum HostRequest {
137 Load {
138 lib: PathBuf,
139 mod_root: Option<PathBuf>,
140 },
141 Render(RenderReq),
142 Png {
143 req: RenderReq,
144 scale: Option<u32>,
145 out: PathBuf,
146 },
147}
148
149#[derive(Debug, Clone, Schema)]
151#[data(tag = "answer")]
152pub enum HostAnswer {
153 Loaded { components: Vec<ComponentInfo> },
154 Drawn { w: f32, h: f32, ops: Vec<DrawOp> },
155 Written { w: f32, h: f32, out: PathBuf },
156}
157
158pub type HostReply = Result<HostAnswer, String>;
160
161pub fn next_answer(out: &mut impl BufRead) -> io::Result<Option<HostReply>> {
164 let mut seen = Vec::new();
165 let last = MARK[MARK.len() - 1];
166 loop {
167 if out.read_until(last, &mut seen)? == 0 {
168 return Ok(None);
169 }
170 if seen.ends_with(MARK) {
171 return yog_data::io::read_frame(out);
172 }
173 }
174}
175
176struct Host {
177 lib: Option<Loaded>,
178 mod_root: Option<PathBuf>,
179}
180
181impl Host {
182 fn handle(&mut self, request: HostRequest) -> HostReply {
183 match request {
184 HostRequest::Load { lib, mod_root } => {
185 if self.lib.is_some() {
186 return Err("a library is already loaded (restart the host to reload)".into());
187 }
188 images::set_document(mod_root.as_deref());
189 self.mod_root = mod_root;
190 let loaded = Loaded::open(&lib)?;
191 let components = loaded.describe()?;
192 self.lib = Some(loaded);
193 Ok(HostAnswer::Loaded { components })
194 }
195 HostRequest::Render(req) => self
196 .loaded()?
197 .render(&req)
198 .map(|(w, h, ops)| HostAnswer::Drawn { w, h, ops }),
199 HostRequest::Png { req, scale, out } => {
200 let (w, h, ops) = self.loaded()?.render(&req)?;
201 let res = super::assets::Resources::for_mod(self.mod_root.as_deref());
202 let (cw, ch) = (w.ceil().max(1.0) as u32, h.ceil().max(1.0) as u32);
203 let scale = scale.unwrap_or(PNG_SCALE).clamp(1, MAX_PNG_SCALE);
204 let canvas = raster::rasterize(&ops, cw, ch, scale, &res);
205 std::fs::write(&out, canvas.png()?)
206 .map_err(|e| format!("{}: {e}", out.display()))?;
207 Ok(HostAnswer::Written { w, h, out })
208 }
209 }
210 }
211
212 fn loaded(&self) -> Result<&Loaded, String> {
213 self.lib
214 .as_ref()
215 .ok_or_else(|| "no library loaded".to_string())
216 }
217}
218
219pub const COMMAND: &str = "designer-host";
221
222pub fn serve() {
224 let mut host = Host {
225 lib: None,
226 mod_root: None,
227 };
228 let mut input = io::stdin().lock();
229 while let Ok(Some(bytes)) = yog_data::io::read_frame_bytes(&mut input) {
230 let reply = match yog_data::read_bytes::<HostRequest>(&bytes) {
231 Ok(request) => host.handle(request),
232 Err(e) => Err(format!("bad request: {e:?}")),
233 };
234 let mut out = io::stdout().lock();
235 let sent = out
236 .write_all(MARK)
237 .and_then(|()| yog_data::io::write_frame(&mut out, &reply));
238 if sent.is_err() {
239 break;
240 }
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn requests_are_answered() {
250 let mut host = Host {
251 lib: None,
252 mod_root: None,
253 };
254 let render = HostRequest::Render(RenderReq {
255 name: "X".into(),
256 attrs: Vec::new(),
257 w: 0.0,
258 h: 0.0,
259 avail_w: 1.0,
260 avail_h: 1.0,
261 });
262 assert_eq!(host.handle(render).unwrap_err(), "no library loaded");
263 let load = HostRequest::Load {
264 lib: "/nowhere/libnope.so".into(),
265 mod_root: None,
266 };
267 assert!(host.handle(load).unwrap_err().contains("can't load"));
268 }
269
270 #[test]
271 fn answers_are_found_past_what_the_mod_printed() {
272 let mut out = b"the mod says hi\n".to_vec();
273 out.extend_from_slice(MARK);
274 yog_data::io::write_frame(&mut out, &HostReply::Err("no".into())).unwrap();
275 let mut r = out.as_slice();
276 assert_eq!(next_answer(&mut r).unwrap().unwrap().unwrap_err(), "no");
277 assert!(next_answer(&mut r).unwrap().is_none());
278 }
279
280 #[test]
283 #[ignore]
284 fn renders_a_built_mod() {
285 let (Ok(lib), Ok(name)) = (
286 std::env::var(yog_ids::env::DESIGNER_LIB),
287 std::env::var(yog_ids::env::DESIGNER_COMPONENT),
288 ) else {
289 return;
290 };
291 let props = std::env::var(yog_ids::env::DESIGNER_PROPS).unwrap_or_default();
292 let props = props
293 .split(';')
294 .filter_map(|kv| kv.split_once('='))
295 .map(|(k, v)| (k.to_string(), v.to_string()))
296 .collect();
297 let mut host = Host {
298 lib: None,
299 mod_root: None,
300 };
301 let loaded = host.handle(HostRequest::Load {
302 lib: lib.into(),
303 mod_root: None,
304 });
305 eprintln!("{loaded:?}");
306 let l = host.loaded().unwrap();
307 let (w, h, ops) = l
308 .render(&RenderReq {
309 name: name.clone(),
310 attrs: props,
311 w: 0.0,
312 h: 0.0,
313 avail_w: 400.0,
314 avail_h: 300.0,
315 })
316 .unwrap();
317 eprintln!("{w}x{h}: {ops:?}");
318 assert!(w > 0.0 && h > 0.0 && !ops.is_empty());
319 if let Ok(out) = std::env::var(yog_ids::env::DESIGNER_PNG) {
320 let res = super::super::assets::Resources::for_mod(None);
321 let canvas = raster::rasterize(&ops, w.ceil() as u32, h.ceil() as u32, 6, &res);
322 std::fs::write(&out, canvas.png().unwrap()).unwrap();
323 }
324 }
325}