Skip to main content

yog_cli/design/custom/
fit.rs

1//! Fitting the mod's components into the designer's picture: each laid out
2//! in its slot, drawn by the mod's library, spliced in where it goes.
3
4use 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
15/// A custom element of the document being drawn.
16pub(super) struct Slot {
17    uid: String,
18    marker: u32,
19    pub(super) req: RenderReq,
20    /// Why it can't be drawn (placeholder text).
21    problem: Option<String>,
22}
23
24/// The document with custom elements replaced by marked spacers.
25pub 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
37/// A component's stand-in color: alpha 1 (never a real color in a .yui,
38/// invisible if left behind), a tag, and the component's index.
39const 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
55/// The document as yog-ui should draw it: a `<Component>` document as a
56/// screen of its design size; markup-only component instances expanded;
57/// each compiled one (or unknown) measured through the host (its natural
58/// size, when the element doesn't size it in px) and swapped for a marked
59/// spacer.
60pub 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
86/// How deep components may nest (a component using itself stops here).
87const 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                // The body's root may itself be a component.
110                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    /// Why a compiled component can't be drawn right now, if it can't.
127    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
223/// Replaces each marker fill in `ops` with the component drawn at its
224/// element's laid-out rect (`rects`: uid → `x y w h`), clipped to the part
225/// that was visible (the marker fill, already cut to scroll views).
226pub 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}