1use std::collections::HashMap;
5use std::path::{Path, PathBuf};
6use yog_sync::Arc;
7
8use super::placeholder::{BUILDING, PLACEHOLDER_H, PLACEHOLDER_W, PROBLEM_H, placeholder};
9use super::{LAYOUT_ATTRS, Snapshot, Status, is_component, state, status};
10use crate::design::components::{self, Def};
11use crate::design::host::RenderReq;
12use crate::design::markup::YuiNode;
13use crate::design::raster::DrawOp;
14
15pub(super) struct Slot {
17 uid: String,
18 marker: u32,
19 pub(super) req: RenderReq,
20 problem: Option<String>,
22}
23
24pub struct Prepared {
26 pub root: YuiNode,
27 pub(super) slots: Vec<Slot>,
28 mod_root: Option<PathBuf>,
29}
30
31impl Prepared {
32 pub fn is_empty(&self) -> bool {
33 self.slots.is_empty()
34 }
35}
36
37const MARKER: u32 = 0x01D0_0000;
40const MARKER_INDEX: u32 = 0xFFFF;
41
42pub(super) fn marker(i: usize) -> u32 {
43 MARKER | (i as u32 & MARKER_INDEX)
44}
45
46fn px(v: Option<&str>) -> Option<f32> {
47 let v = v?.trim();
48 if v.ends_with('%') {
49 None
50 } else {
51 v.parse().ok()
52 }
53}
54
55pub fn prepare(root: &YuiNode, mod_root: Option<&Path>, screen_w: f32, screen_h: f32) -> Prepared {
61 let root = if root.kind == yog_ids::yui::COMPONENT {
62 components::design_root(root)
63 } else {
64 root.clone()
65 };
66 let mut prepared = Prepared {
67 root,
68 slots: Vec::new(),
69 mod_root: mod_root.map(Path::to_path_buf),
70 };
71 if !has_custom(&prepared.root) {
72 return prepared;
73 }
74 let mut cx = Cx {
75 mod_root,
76 defs: mod_root.map(components::scan).unwrap_or_default(),
77 snapshot: None,
78 slots: Vec::new(),
79 screen: (screen_w, screen_h),
80 };
81 cx.process(&mut prepared.root, 0);
82 prepared.slots = cx.slots;
83 prepared
84}
85
86const MAX_NESTING: usize = 16;
88
89struct Cx<'a> {
90 mod_root: Option<&'a Path>,
91 defs: Arc<Vec<Def>>,
92 snapshot: Option<Snapshot>,
93 slots: Vec<Slot>,
94 screen: (f32, f32),
95}
96
97impl Cx<'_> {
98 fn process(&mut self, node: &mut YuiNode, depth: usize) {
99 if !is_component(&node.kind) {
100 for child in &mut node.children {
101 self.process(child, depth);
102 }
103 return;
104 }
105 let defs = self.defs.clone();
106 match Def::find(&defs, &node.kind) {
107 Some(def) if !def.compiled && depth < MAX_NESTING => {
108 *node = components::expand(def, node);
109 self.process(node, depth + 1);
111 }
112 def => {
113 let problem = match def {
114 None => Some(format!(
115 "no component <{}> in this mod — add one: New Item → Component (.yui)",
116 node.kind
117 )),
118 Some(d) if !d.compiled => Some("this component contains itself".to_string()),
119 Some(_) => self.library_problem(&node.kind),
120 };
121 self.slot(node, problem);
122 }
123 }
124 }
125
126 fn library_problem(&mut self, name: &str) -> Option<String> {
128 let Some(mod_root) = self.mod_root else {
129 return Some("not inside a mod (no src/ folder)".into());
130 };
131 let s = self.snapshot.get_or_insert_with(|| status(mod_root));
132 match s.status {
133 Status::Building | Status::None => Some(format!("{BUILDING} designer library…")),
134 Status::Failed => Some(format!(
135 "The designer could not load the mod's library:\n{}",
136 s.error.clone().unwrap_or_default()
137 )),
138 Status::Ready => {
139 let registered = state(mod_root)
140 .lock()
141 .components
142 .iter()
143 .any(|c| c.name == name);
144 (!registered).then(|| format!("the mod's library doesn't register <{name}> — does its code-behind call yog_api::ui_include!?"))
145 }
146 }
147 }
148
149 fn slot(&mut self, node: &mut YuiNode, problem: Option<String>) {
150 let name = node.kind.clone();
151 let props: Vec<(String, String)> = node
152 .attrs
153 .iter()
154 .filter(|(k, _)| !LAYOUT_ATTRS.contains(&k.as_str()))
155 .map(|(k, v)| (k.clone(), v.clone()))
156 .collect();
157 let (w, h) = (
158 px(node.attr(yog_ids::yui_attr::W)),
159 px(node.attr(yog_ids::yui_attr::H)),
160 );
161 let req = RenderReq {
162 name,
163 attrs: props,
164 w: w.unwrap_or(0.0),
165 h: h.unwrap_or(0.0),
166 avail_w: self.screen.0,
167 avail_h: self.screen.1,
168 };
169 let (mut nw, mut nh) = (
170 w.unwrap_or(PLACEHOLDER_W),
171 h.unwrap_or(if problem.is_some() {
172 PROBLEM_H
173 } else {
174 PLACEHOLDER_H
175 }),
176 );
177 let problem = match (problem, self.mod_root) {
178 (None, Some(root)) if w.is_none() || h.is_none() => {
179 match state(root).lock().render(&req) {
180 Ok((mw, mh, _)) => {
181 nw = w.unwrap_or(mw);
182 nh = h.unwrap_or(mh);
183 None
184 }
185 Err(e) => Some(e),
186 }
187 }
188 (p, _) => p,
189 };
190 let i = self.slots.len();
191 let mut attrs: HashMap<String, String> = node
192 .attrs
193 .iter()
194 .filter(|(k, _)| LAYOUT_ATTRS.contains(&k.as_str()))
195 .map(|(k, v)| (k.clone(), v.clone()))
196 .collect();
197 attrs
198 .entry(yog_ids::yui_attr::W.into())
199 .or_insert_with(|| nw.to_string());
200 attrs
201 .entry(yog_ids::yui_attr::H.into())
202 .or_insert_with(|| nh.to_string());
203 attrs.insert(yog_ids::yui_attr::BG.into(), format!("#{:08X}", marker(i)));
204 self.slots.push(Slot {
205 uid: node.uid.clone(),
206 marker: marker(i),
207 req,
208 problem,
209 });
210 *node = YuiNode {
211 uid: node.uid.clone(),
212 kind: yog_ids::yui::SPACER.into(),
213 attrs,
214 children: Vec::new(),
215 };
216 }
217}
218
219fn has_custom(node: &YuiNode) -> bool {
220 is_component(&node.kind) || node.children.iter().any(has_custom)
221}
222
223pub fn splice(ops: &mut Vec<DrawOp>, prepared: &Prepared, rects: &HashMap<String, [f32; 4]>) {
227 if prepared.slots.is_empty() {
228 return;
229 }
230 let shared = prepared.mod_root.as_deref().map(state);
231 let mut out = Vec::with_capacity(ops.len());
232 for op in ops.drain(..) {
233 let DrawOp::Rect {
234 x1,
235 y1,
236 x2,
237 y2,
238 color,
239 } = op
240 else {
241 out.push(op);
242 continue;
243 };
244 let Some(slot) = prepared.slots.iter().find(|s| s.marker == color) else {
245 out.push(op);
246 continue;
247 };
248 let [x, y, w, h] = rects
249 .get(&slot.uid)
250 .copied()
251 .unwrap_or([x1, y1, x2 - x1, y2 - y1]);
252 let drawn = match (&slot.problem, &shared) {
253 (Some(p), _) => Err(p.clone()),
254 (None, None) => Err("not inside a mod".into()),
255 (None, Some(sh)) => sh
256 .lock()
257 .render(&RenderReq {
258 w,
259 h,
260 ..slot.req.clone()
261 })
262 .map(|(_, _, ops)| ops),
263 };
264 let inner = drawn.unwrap_or_else(|e| placeholder(&slot.req.name, &e, w, h));
265 out.push(DrawOp::Scissor {
266 x: x1.floor() as i32,
267 y: y1.floor() as i32,
268 w: (x2.ceil() - x1.floor()) as i32,
269 h: (y2.ceil() - y1.floor()) as i32,
270 });
271 out.extend(inner.into_iter().map(|o| o.moved(x, y)));
272 out.push(DrawOp::ClearScissor);
273 }
274 *ops = out;
275}