use std::ffi::c_void;
use std::panic::{AssertUnwindSafe, catch_unwind};
use yog_abi::{YogBytes, YogGfxApi};
use yog_data::Schema;
use yog_gfx::GfxContext;
use super::{COMMON_ATTRS, PropKind, Props, apply_style, components, find};
use crate::widget::{self, Widget};
use crate::{FlexDir, UiRoot, Units};
pub const ABI: u32 = 2;
pub type WriteFn = unsafe extern "C" fn(user_data: *mut c_void, reply: YogBytes);
#[derive(Debug, Clone, PartialEq, Schema)]
#[data(tag = "request")]
pub enum Request {
Describe,
Render(Render),
}
#[derive(Debug, Clone, PartialEq, Schema)]
pub struct Render {
pub name: String,
#[data(default)]
pub attrs: Vec<(String, String)>,
pub w: f32,
pub h: f32,
pub avail_w: f32,
pub avail_h: f32,
}
#[derive(Debug, Clone, PartialEq, Schema)]
#[data(tag = "answer")]
pub enum Answer {
Described { components: Vec<ComponentInfo> },
Rendered { w: f32, h: f32 },
}
pub type Reply = Result<Answer, String>;
#[derive(Debug, Clone, PartialEq, Schema)]
pub struct ComponentInfo {
pub name: String,
#[data(default)]
pub description: String,
#[data(default)]
pub display_name: String,
#[data(default)]
pub icon: String,
#[data(default)]
pub category: String,
#[data(default)]
pub design_w: f32,
#[data(default)]
pub design_h: f32,
#[data(default = "yes")]
pub toolbox: bool,
#[data(default)]
pub props: Vec<PropInfo>,
#[data(default)]
pub file: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Schema)]
pub struct PropInfo {
pub name: String,
pub kind: String,
#[data(default)]
pub default: String,
#[data(default)]
pub doc: String,
#[data(default)]
pub category: String,
#[data(default)]
pub options: Vec<String>,
}
fn yes() -> bool {
true
}
pub unsafe fn call(
request: YogBytes,
gfx: *const YogGfxApi,
user_data: *mut c_void,
write: WriteFn,
) -> bool {
let request = unsafe { request.as_slice() };
let reply: Reply = match catch_unwind(AssertUnwindSafe(|| answer(request, gfx))) {
Ok(reply) => reply,
Err(panic) => Err(panic_message(&*panic)),
};
let bytes = yog_data::to_bytes(&reply);
unsafe { write(user_data, YogBytes::new(&bytes)) };
reply.is_ok()
}
fn panic_message(panic: &(dyn std::any::Any + Send)) -> String {
let msg = panic
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| panic.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "unknown panic".into());
format!("the component panicked: {msg}")
}
fn answer(request: &[u8], gfx: *const YogGfxApi) -> Reply {
let request = yog_data::read_bytes::<Request>(request)
.map_err(|e| format!("not a designer request: {e:?}"))?;
match request {
Request::Describe => Ok(describe()),
Request::Render(r) => {
if gfx.is_null() {
return Err("render: no gfx table".into());
}
let ctx = unsafe { GfxContext::from_raw(gfx) };
render(&ctx, &r)
}
}
}
fn render(ctx: &GfxContext, r: &Render) -> Reply {
let spec = find(&r.name).ok_or_else(|| format!("no component named {}", r.name))?;
let (w, h, aw, ah) = (r.w, r.h, r.avail_w, r.avail_h);
type Attrs<'a> = Vec<(&'a str, &'a str)>;
let (common, own): (Attrs, Attrs) = r
.attrs
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.partition(|(k, _)| COMMON_ATTRS.contains(k));
let mut root: Widget = (spec.build)(&Props::new(spec.props, &own));
for (k, v) in common {
root = apply_style(root, k, v);
}
if w > 0.0 {
root = root.w(Units::Px(w));
}
if h > 0.0 {
root = root.h(Units::Px(h));
}
let (aw, ah) = (aw.max(1.0), ah.max(1.0));
let screen = widget::panel(FlexDir::Column)
.w(Units::Px(aw))
.h(Units::Px(ah))
.child(root.at(0.0, 0.0));
crate::text::use_host_metrics(ctx);
crate::image::use_host_images(ctx);
let mut ui = UiRoot::new(yog_ids::yog::DESIGNER_SCREEN, screen);
ui.layout(aw, ah);
let rect = ui
.layout_root
.children
.first()
.map(|n| n.rect)
.unwrap_or_default();
ui.render(ctx);
Ok(Answer::Rendered {
w: rect.w,
h: rect.h,
})
}
fn describe() -> Answer {
let components = components()
.iter()
.map(|c| ComponentInfo {
name: c.name.into(),
description: c.doc.into(),
display_name: c.display_name.into(),
icon: c.icon.into(),
category: c.category.into(),
design_w: c.design_size.0,
design_h: c.design_size.1,
toolbox: c.toolbox,
props: c
.props
.iter()
.map(|p| PropInfo {
name: p.name.into(),
kind: p.kind.as_str().into(),
default: p.default.into(),
doc: p.doc.into(),
category: p.category.into(),
options: match p.kind {
PropKind::Choice(o) => o.iter().map(|s| s.to_string()).collect(),
_ => Vec::new(),
},
})
.collect(),
file: None,
})
.collect();
Answer::Described { components }
}