use std::ffi::c_void;
use std::io::{self, BufRead, Write};
use std::path::{Path, PathBuf};
use yog_abi::{YogBytes, YogGfxApi};
use yog_data::Schema;
use yog_ui::component::designer::{self as protocol, Answer};
pub use yog_ui::component::designer::{ComponentInfo, PropInfo, Render as RenderReq};
use super::raster::DrawOp;
use super::{gfx, images, raster};
pub const MARK: &[u8] = b"\x01yog-designer\x01";
pub const DESIGNER_ABI: u32 = protocol::ABI;
const PNG_SCALE: u32 = 4;
const MAX_PNG_SCALE: u32 = 8;
type DesignerFn =
unsafe extern "C" fn(YogBytes, *const YogGfxApi, *mut c_void, protocol::WriteFn) -> bool;
pub struct Loaded {
_lib: libloading::Library,
call: DesignerFn,
}
unsafe extern "C" fn sink(user_data: *mut c_void, reply: YogBytes) {
unsafe { (*(user_data as *mut Vec<u8>)).extend_from_slice(reply.as_slice()) };
}
impl Loaded {
pub fn open(lib: &Path) -> Result<Self, String> {
let lib = unsafe { libloading::Library::new(lib) }
.map_err(|e| format!("can't load {}: {e}", lib.display()))?;
unsafe {
let abi = lib
.get::<unsafe extern "C" fn() -> u32>(b"yog_abi_version\0")
.map_err(|_| {
"not a Yog mod library (no yog_abi_version — is `export_mod!` missing?)"
.to_string()
})?;
let abi = abi();
if abi != yog_abi::ABI_VERSION {
return Err(format!(
"the mod is built against Yog ABI {abi}, the designer against {} — update yog-api (or Yog-IDLE) so they match",
yog_abi::ABI_VERSION
));
}
let designer = lib
.get::<unsafe extern "C" fn() -> u32>(b"yog_ui_designer_abi\0")
.map_err(|_| {
"the mod's yog-api has no designer support (yog_ui_designer) — update yog-api"
.to_string()
})?;
let designer = designer();
if designer != DESIGNER_ABI {
return Err(format!(
"designer protocol {designer} (this IDE speaks {DESIGNER_ABI}) — update yog-api or Yog-IDLE"
));
}
let call = *lib
.get::<DesignerFn>(b"yog_ui_designer\0")
.map_err(|e| e.to_string())?;
Ok(Self { _lib: lib, call })
}
}
fn ask(&self, request: &protocol::Request, gfx: *const YogGfxApi) -> Result<Answer, String> {
let request = yog_data::to_bytes(request);
let mut reply = Vec::new();
unsafe {
(self.call)(
YogBytes::new(&request),
gfx,
&mut reply as *mut Vec<u8> as *mut c_void,
sink,
)
};
yog_data::read_bytes::<protocol::Reply>(&reply)
.map_err(|e| format!("a mod's answer that isn't one: {e:?}"))?
}
pub fn describe(&self) -> Result<Vec<ComponentInfo>, String> {
match self.ask(&protocol::Request::Describe, std::ptr::null())? {
Answer::Described { components } => Ok(components),
other => Err(format!("asked to describe, the mod answered {other:?}")),
}
}
pub fn render(&self, req: &RenderReq) -> Result<(f32, f32, Vec<DrawOp>), String> {
let (aw, ah) = (req.avail_w.max(1.0) as i32, req.avail_h.max(1.0) as i32);
let mut answer = Err(String::new());
let table = gfx::api(aw, ah, 1.0);
let request = protocol::Request::Render(req.clone());
let ops = gfx::record(aw, ah, |_| answer = self.ask(&request, &table));
match answer? {
Answer::Rendered { w, h } => Ok((w, h, ops)),
other => Err(format!("asked to render, the mod answered {other:?}")),
}
}
}
#[derive(Debug, Clone, Schema)]
#[data(tag = "cmd")]
pub enum HostRequest {
Load {
lib: PathBuf,
mod_root: Option<PathBuf>,
},
Render(RenderReq),
Png {
req: RenderReq,
scale: Option<u32>,
out: PathBuf,
},
}
#[derive(Debug, Clone, Schema)]
#[data(tag = "answer")]
pub enum HostAnswer {
Loaded { components: Vec<ComponentInfo> },
Drawn { w: f32, h: f32, ops: Vec<DrawOp> },
Written { w: f32, h: f32, out: PathBuf },
}
pub type HostReply = Result<HostAnswer, String>;
pub fn next_answer(out: &mut impl BufRead) -> io::Result<Option<HostReply>> {
let mut seen = Vec::new();
let last = MARK[MARK.len() - 1];
loop {
if out.read_until(last, &mut seen)? == 0 {
return Ok(None);
}
if seen.ends_with(MARK) {
return yog_data::io::read_frame(out);
}
}
}
struct Host {
lib: Option<Loaded>,
mod_root: Option<PathBuf>,
}
impl Host {
fn handle(&mut self, request: HostRequest) -> HostReply {
match request {
HostRequest::Load { lib, mod_root } => {
if self.lib.is_some() {
return Err("a library is already loaded (restart the host to reload)".into());
}
images::set_document(mod_root.as_deref());
self.mod_root = mod_root;
let loaded = Loaded::open(&lib)?;
let components = loaded.describe()?;
self.lib = Some(loaded);
Ok(HostAnswer::Loaded { components })
}
HostRequest::Render(req) => self
.loaded()?
.render(&req)
.map(|(w, h, ops)| HostAnswer::Drawn { w, h, ops }),
HostRequest::Png { req, scale, out } => {
let (w, h, ops) = self.loaded()?.render(&req)?;
let res = super::assets::Resources::for_mod(self.mod_root.as_deref());
let (cw, ch) = (w.ceil().max(1.0) as u32, h.ceil().max(1.0) as u32);
let scale = scale.unwrap_or(PNG_SCALE).clamp(1, MAX_PNG_SCALE);
let canvas = raster::rasterize(&ops, cw, ch, scale, &res);
std::fs::write(&out, canvas.png()?)
.map_err(|e| format!("{}: {e}", out.display()))?;
Ok(HostAnswer::Written { w, h, out })
}
}
}
fn loaded(&self) -> Result<&Loaded, String> {
self.lib
.as_ref()
.ok_or_else(|| "no library loaded".to_string())
}
}
pub const COMMAND: &str = "designer-host";
pub fn serve() {
let mut host = Host {
lib: None,
mod_root: None,
};
let mut input = io::stdin().lock();
while let Ok(Some(bytes)) = yog_data::io::read_frame_bytes(&mut input) {
let reply = match yog_data::read_bytes::<HostRequest>(&bytes) {
Ok(request) => host.handle(request),
Err(e) => Err(format!("bad request: {e:?}")),
};
let mut out = io::stdout().lock();
let sent = out
.write_all(MARK)
.and_then(|()| yog_data::io::write_frame(&mut out, &reply));
if sent.is_err() {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn requests_are_answered() {
let mut host = Host {
lib: None,
mod_root: None,
};
let render = HostRequest::Render(RenderReq {
name: "X".into(),
attrs: Vec::new(),
w: 0.0,
h: 0.0,
avail_w: 1.0,
avail_h: 1.0,
});
assert_eq!(host.handle(render).unwrap_err(), "no library loaded");
let load = HostRequest::Load {
lib: "/nowhere/libnope.so".into(),
mod_root: None,
};
assert!(host.handle(load).unwrap_err().contains("can't load"));
}
#[test]
fn answers_are_found_past_what_the_mod_printed() {
let mut out = b"the mod says hi\n".to_vec();
out.extend_from_slice(MARK);
yog_data::io::write_frame(&mut out, &HostReply::Err("no".into())).unwrap();
let mut r = out.as_slice();
assert_eq!(next_answer(&mut r).unwrap().unwrap().unwrap_err(), "no");
assert!(next_answer(&mut r).unwrap().is_none());
}
#[test]
#[ignore]
fn renders_a_built_mod() {
let (Ok(lib), Ok(name)) = (
std::env::var(yog_ids::env::DESIGNER_LIB),
std::env::var(yog_ids::env::DESIGNER_COMPONENT),
) else {
return;
};
let props = std::env::var(yog_ids::env::DESIGNER_PROPS).unwrap_or_default();
let props = props
.split(';')
.filter_map(|kv| kv.split_once('='))
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
let mut host = Host {
lib: None,
mod_root: None,
};
let loaded = host.handle(HostRequest::Load {
lib: lib.into(),
mod_root: None,
});
eprintln!("{loaded:?}");
let l = host.loaded().unwrap();
let (w, h, ops) = l
.render(&RenderReq {
name: name.clone(),
attrs: props,
w: 0.0,
h: 0.0,
avail_w: 400.0,
avail_h: 300.0,
})
.unwrap();
eprintln!("{w}x{h}: {ops:?}");
assert!(w > 0.0 && h > 0.0 && !ops.is_empty());
if let Ok(out) = std::env::var(yog_ids::env::DESIGNER_PNG) {
let res = super::super::assets::Resources::for_mod(None);
let canvas = raster::rasterize(&ops, w.ceil() as u32, h.ceil() as u32, 6, &res);
std::fs::write(&out, canvas.png().unwrap()).unwrap();
}
}
}