1use boa_engine::property::Attribute;
2use boa_engine::NativeFunction;
3use boa_engine::{Context, JsArgs, JsValue, Source};
4use std::cell::RefCell;
5use std::rc::Rc;
6
7use crate::scene::*;
8
9#[derive(Debug, Clone)]
10pub enum DomMutation {
11 SetTextContent(String, String),
12 SetStyle(String, String, String),
13 AddClass(String, String),
14 RemoveClass(String, String),
15}
16
17pub struct JsEngine {
18 context: Context,
19 mutations: Rc<RefCell<Vec<DomMutation>>>,
20}
21
22impl Default for JsEngine {
23 fn default() -> Self {
24 Self::new()
25 }
26}
27
28impl JsEngine {
29 pub fn new() -> Self {
30 let context = Context::default();
31 let mutations = Rc::new(RefCell::new(Vec::new()));
32
33 let mut engine = Self { context, mutations };
34 engine.register_dom_api();
35 engine
36 }
37
38 fn register_dom_api(&mut self) {
39 let mutations = self.mutations.clone();
40
41 let get_element = unsafe {
45 NativeFunction::from_closure(move |_, args, ctx| {
46 let id = args
47 .get_or_undefined(0)
48 .to_string(ctx)?
49 .to_std_string_escaped();
50
51 let m = mutations.clone();
52 let element_id = id.clone();
53
54 let obj = boa_engine::JsObject::with_null_proto();
55
56 {
58 let m = m.clone();
59 let eid = element_id.clone();
60 let func = NativeFunction::from_closure(move |_, args, ctx| {
61 let text = args
62 .get_or_undefined(0)
63 .to_string(ctx)?
64 .to_std_string_escaped();
65 m.borrow_mut()
66 .push(DomMutation::SetTextContent(eid.clone(), text));
67 Ok(JsValue::undefined())
68 });
69 let js_func = func.to_js_function(ctx.realm());
70 obj.set(
71 boa_engine::js_string!("setTextContent"),
72 js_func,
73 false,
74 ctx,
75 )?;
76 }
77
78 {
80 let m = m.clone();
81 let eid = element_id.clone();
82 let func = NativeFunction::from_closure(move |_, args, ctx| {
83 let prop = args
84 .get_or_undefined(0)
85 .to_string(ctx)?
86 .to_std_string_escaped();
87 let val = args
88 .get_or_undefined(1)
89 .to_string(ctx)?
90 .to_std_string_escaped();
91 m.borrow_mut()
92 .push(DomMutation::SetStyle(eid.clone(), prop, val));
93 Ok(JsValue::undefined())
94 });
95 let js_func = func.to_js_function(ctx.realm());
96 obj.set(boa_engine::js_string!("setStyle"), js_func, false, ctx)?;
97 }
98
99 {
101 let m = m.clone();
102 let eid = element_id.clone();
103 let func = NativeFunction::from_closure(move |_, args, ctx| {
104 let class = args
105 .get_or_undefined(0)
106 .to_string(ctx)?
107 .to_std_string_escaped();
108 m.borrow_mut()
109 .push(DomMutation::AddClass(eid.clone(), class));
110 Ok(JsValue::undefined())
111 });
112 let js_func = func.to_js_function(ctx.realm());
113 obj.set(boa_engine::js_string!("addClass"), js_func, false, ctx)?;
114 }
115
116 {
118 let m = m.clone();
119 let eid = element_id.clone();
120 let func = NativeFunction::from_closure(move |_, args, ctx| {
121 let class = args
122 .get_or_undefined(0)
123 .to_string(ctx)?
124 .to_std_string_escaped();
125 m.borrow_mut()
126 .push(DomMutation::RemoveClass(eid.clone(), class));
127 Ok(JsValue::undefined())
128 });
129 let js_func = func.to_js_function(ctx.realm());
130 obj.set(boa_engine::js_string!("removeClass"), js_func, false, ctx)?;
131 }
132
133 obj.set(
134 boa_engine::js_string!("id"),
135 JsValue::from(boa_engine::js_string!(element_id.as_str())),
136 false,
137 ctx,
138 )?;
139
140 Ok(JsValue::from(obj))
141 })
142 };
143
144 let document = boa_engine::JsObject::with_null_proto();
145 let get_elem_func = get_element.to_js_function(self.context.realm());
146 document
147 .set(
148 boa_engine::js_string!("getElementById"),
149 get_elem_func,
150 false,
151 &mut self.context,
152 )
153 .ok();
154
155 self.context
156 .register_global_property(
157 boa_engine::js_string!("document"),
158 JsValue::from(document),
159 Attribute::all(),
160 )
161 .ok();
162
163 let console = boa_engine::JsObject::with_null_proto();
165 let log_fn = NativeFunction::from_fn_ptr(|_, args, ctx| {
166 let msg = args
167 .get_or_undefined(0)
168 .to_string(ctx)?
169 .to_std_string_escaped();
170 println!("[foundry:js] {}", msg);
171 Ok(JsValue::undefined())
172 });
173 let log_js_func = log_fn.to_js_function(self.context.realm());
174 console
175 .set(
176 boa_engine::js_string!("log"),
177 log_js_func,
178 false,
179 &mut self.context,
180 )
181 .ok();
182
183 self.context
184 .register_global_property(
185 boa_engine::js_string!("console"),
186 JsValue::from(console),
187 Attribute::all(),
188 )
189 .ok();
190 }
191
192 pub fn execute(&mut self, code: &str) -> Result<(), String> {
193 self.mutations.borrow_mut().clear();
194 self.context
196 .eval(Source::from_bytes(code))
197 .map_err(|e| format!("JS error: {}", e))?;
198 Ok(())
199 }
200
201 pub fn take_mutations(&self) -> Vec<DomMutation> {
202 self.mutations.borrow_mut().drain(..).collect()
203 }
204
205 pub fn apply_mutations(&self, scene: &mut SceneGraph, mutations: &[DomMutation]) {
206 for mutation in mutations {
207 match mutation {
208 DomMutation::SetTextContent(element_id, text) => {
209 if let Some(node_id) = scene.find_by_element_id(element_id) {
210 let children: Vec<NodeId> = scene.get(node_id).children.clone();
211 let text_child = children
212 .iter()
213 .find(|&&c| scene.get(c).kind == ElementKind::Text);
214 if let Some(&child_id) = text_child {
215 scene.get_mut(child_id).text_content = Some(text.clone());
216 scene.get_mut(child_id).dirty = true;
217 } else {
218 let child = scene.add_node(ElementKind::Text, "text".to_string());
219 scene.get_mut(child).text_content = Some(text.clone());
220 scene.add_child(node_id, child);
221 }
222 scene.mark_dirty_recursive(node_id);
223 }
224 }
225 DomMutation::SetStyle(element_id, prop, val) => {
226 if let Some(node_id) = scene.find_by_element_id(element_id) {
227 crate::css::apply_property(&mut scene.get_mut(node_id).style, prop, val);
228 scene.mark_dirty_recursive(node_id);
229 }
230 }
231 DomMutation::AddClass(element_id, class) => {
232 if let Some(node_id) = scene.find_by_element_id(element_id) {
233 let node = scene.get_mut(node_id);
234 if !node.classes.contains(class) {
235 node.classes.push(class.clone());
236 node.dirty = true;
237 }
238 }
239 }
240 DomMutation::RemoveClass(element_id, class) => {
241 if let Some(node_id) = scene.find_by_element_id(element_id) {
242 let node = scene.get_mut(node_id);
243 node.classes.retain(|c| c != class);
244 node.dirty = true;
245 }
246 }
247 }
248 }
249 }
250}