1use lumen_core::node::{DomIndex, DomRecord, NodeHandle, dom_index_snapshot};
17use lumen_ir::css::AncestorInfo;
18
19use bevy_ecs::entity::Entity;
20use std::collections::HashMap;
21use std::sync::{Arc, Mutex, OnceLock, RwLock};
22
23use crate::ScriptCommand;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub struct NodeRef(pub u64);
28
29impl NodeRef {
30 pub const NONE: NodeRef = NodeRef(0);
32
33 pub fn from_entity(entity: Entity) -> Self {
35 Self(NodeHandle::new(entity).pack())
36 }
37
38 pub fn entity(self) -> Option<Entity> {
41 if self.0 == 0 || lumen_core::node::is_reserved_token(self.0) {
42 return None;
43 }
44 NodeHandle::unpack(self.0).map(|h| h.entity)
45 }
46}
47
48impl From<Entity> for NodeRef {
49 fn from(e: Entity) -> Self {
50 Self::from_entity(e)
51 }
52}
53
54#[derive(Debug, Clone, Default)]
57pub struct NodeQueryResult {
58 pub nodes: Vec<u64>,
60}
61
62impl NodeQueryResult {
63 pub fn len(&self) -> usize {
65 self.nodes.len()
66 }
67
68 pub fn is_empty(&self) -> bool {
70 self.nodes.is_empty()
71 }
72
73 pub fn first(&self) -> Option<u64> {
75 self.nodes.first().copied()
76 }
77
78 pub fn nth(&self, index: usize) -> Option<u64> {
80 self.nodes.get(index).copied()
81 }
82
83 pub fn single(&self) -> Result<u64, String> {
86 match self.nodes.len() {
87 1 => Ok(self.nodes[0]),
88 n => Err(format!(
89 "query.single(): expected exactly 1 match, found {n}"
90 )),
91 }
92 }
93
94 pub fn get_single(&self) -> Option<u64> {
97 if self.nodes.len() == 1 {
98 Some(self.nodes[0])
99 } else {
100 None
101 }
102 }
103
104 pub fn collect(&self) -> Vec<u64> {
106 self.nodes.clone()
107 }
108}
109
110fn subject_info(rec: &DomRecord) -> AncestorInfo {
113 AncestorInfo::new(rec.tag.clone(), rec.classes.clone(), rec.id.clone())
114 .with_position(record_position(rec))
115}
116
117pub(crate) fn record_position(rec: &DomRecord) -> lumen_ir::css::SiblingPosition {
119 lumen_ir::css::SiblingPosition::new(rec.child_index, rec.sibling_count)
120}
121
122pub(crate) fn ancestor_infos(index: &DomIndex, entity: Entity) -> Vec<AncestorInfo> {
124 let mut chain: Vec<AncestorInfo> = index
125 .ancestors(entity)
126 .into_iter()
127 .filter_map(|e| index.record(e).map(subject_info))
128 .collect();
129 chain.reverse();
130 chain
131}
132
133fn matches_any(index: &DomIndex, entity: Entity, selectors: &[lumen_ir::css::SelectorBuf]) -> bool {
135 let Some(rec) = index.record(entity) else {
136 return false;
137 };
138 let subject = subject_info(rec);
139 let ancestors = ancestor_infos(index, entity);
140 selectors
141 .iter()
142 .any(|sel| lumen_ir::css::selector_matches(sel, &subject, &ancestors))
143}
144
145pub fn query_entities(index: &DomIndex, selector: &str) -> Result<Vec<Entity>, String> {
148 let selectors = lumen_ir::css::parse_selector_list(selector)?;
149 let mut hits: Vec<(u32, Entity)> = Vec::new();
150 for rec in index.records() {
151 if matches_any(index, rec.entity, &selectors) {
152 hits.push((rec.doc_order, rec.entity));
153 }
154 }
155 hits.sort_by_key(|(order, _)| *order);
156 Ok(hits.into_iter().map(|(_, e)| e).collect())
157}
158
159pub fn closest_entity(
162 index: &DomIndex,
163 start: Entity,
164 selector: &str,
165) -> Result<Option<Entity>, String> {
166 let selectors = lumen_ir::css::parse_selector_list(selector)?;
167 let mut cur = Some(start);
168 while let Some(e) = cur {
169 if matches_any(index, e, &selectors) {
170 return Ok(Some(e));
171 }
172 cur = index.parent(e);
173 }
174 Ok(None)
175}
176
177pub fn run_query(selector: &str) -> Result<NodeQueryResult, String> {
184 let index = dom_index_snapshot();
185 let nodes = query_entities(&index, selector)?
186 .into_iter()
187 .map(|e| NodeHandle::new(e).pack())
188 .collect();
189 Ok(NodeQueryResult { nodes })
190}
191
192pub fn run_get_by_id(id: &str) -> Option<u64> {
194 dom_index_snapshot()
195 .get_by_id(id)
196 .map(|e| NodeHandle::new(e).pack())
197}
198
199pub fn run_document() -> Option<u64> {
201 dom_index_snapshot()
202 .document()
203 .map(|e| NodeHandle::new(e).pack())
204}
205
206fn map_relation<F>(handle: u64, f: F) -> Option<u64>
207where
208 F: FnOnce(&DomIndex, Entity) -> Option<Entity>,
209{
210 let entity = NodeRef(handle).entity()?;
211 let index = dom_index_snapshot();
212 f(&index, entity).map(|e| NodeHandle::new(e).pack())
213}
214
215pub fn node_parent(handle: u64) -> Option<u64> {
217 map_relation(handle, |i, e| i.parent(e))
218}
219
220pub fn node_first_child(handle: u64) -> Option<u64> {
222 map_relation(handle, |i, e| i.first_child(e))
223}
224
225pub fn node_last_child(handle: u64) -> Option<u64> {
227 map_relation(handle, |i, e| i.last_child(e))
228}
229
230pub fn node_next(handle: u64) -> Option<u64> {
232 map_relation(handle, |i, e| i.next_sibling(e))
233}
234
235pub fn node_prev(handle: u64) -> Option<u64> {
237 map_relation(handle, |i, e| i.prev_sibling(e))
238}
239
240pub fn node_children(handle: u64) -> Vec<u64> {
242 let Some(entity) = NodeRef(handle).entity() else {
243 return Vec::new();
244 };
245 dom_index_snapshot()
246 .children(entity)
247 .into_iter()
248 .map(|e| NodeHandle::new(e).pack())
249 .collect()
250}
251
252pub fn node_closest(handle: u64, selector: &str) -> Result<Option<u64>, String> {
254 let Some(entity) = NodeRef(handle).entity() else {
255 return Ok(None);
256 };
257 let index = dom_index_snapshot();
258 Ok(closest_entity(&index, entity, selector)?.map(|e| NodeHandle::new(e).pack()))
259}
260
261pub fn node_valid(handle: u64) -> bool {
265 match NodeRef(handle).entity() {
266 Some(entity) => dom_index_snapshot().record(entity).is_some(),
267 None => false,
268 }
269}
270
271#[derive(Debug, Clone, Default)]
281pub struct NodeDetail {
282 pub text: Option<String>,
284 pub attributes: Vec<(String, String)>,
286 pub inline_style: Vec<(String, String)>,
288 pub markup_style: Vec<(String, String)>,
291}
292
293#[derive(Default)]
296pub struct DomDetails {
297 nodes: HashMap<u64, NodeDetail>,
298 sheet: Option<Arc<lumen_ir::css::Stylesheet>>,
299 media: lumen_ir::css::MediaContext,
300}
301
302impl DomDetails {
303 pub fn new(
306 nodes: HashMap<u64, NodeDetail>,
307 sheet: Option<Arc<lumen_ir::css::Stylesheet>>,
308 media: lumen_ir::css::MediaContext,
309 ) -> Self {
310 Self {
311 nodes,
312 sheet,
313 media,
314 }
315 }
316
317 fn detail(&self, handle: u64) -> Option<&NodeDetail> {
318 self.nodes.get(&handle)
319 }
320
321 pub fn sheet(&self) -> Option<&Arc<lumen_ir::css::Stylesheet>> {
323 self.sheet.as_ref()
324 }
325
326 pub fn media(&self) -> lumen_ir::css::MediaContext {
328 self.media
329 }
330
331 pub fn inline_style_of(&self, handle: u64) -> Vec<(String, String)> {
333 self.detail(handle)
334 .map(|d| d.inline_style.clone())
335 .unwrap_or_default()
336 }
337
338 pub fn attributes_of(&self, handle: u64) -> Vec<(String, String)> {
340 self.detail(handle)
341 .map(|d| d.attributes.clone())
342 .unwrap_or_default()
343 }
344}
345
346static DOM_DETAILS: OnceLock<RwLock<Arc<DomDetails>>> = OnceLock::new();
347
348fn dom_details_cell() -> &'static RwLock<Arc<DomDetails>> {
349 DOM_DETAILS.get_or_init(|| RwLock::new(Arc::new(DomDetails::default())))
350}
351
352pub fn publish_dom_details(details: DomDetails) {
356 if let Ok(mut g) = dom_details_cell().write() {
357 *g = Arc::new(details);
358 }
359}
360
361pub fn dom_details_snapshot() -> Arc<DomDetails> {
363 dom_details_cell()
364 .read()
365 .map(|g| g.clone())
366 .unwrap_or_else(|_| Arc::new(DomDetails::default()))
367}
368
369pub fn node_text(handle: u64) -> Option<String> {
371 dom_details_snapshot()
372 .detail(handle)
373 .and_then(|d| d.text.clone())
374}
375
376pub fn node_get_attr(handle: u64, name: &str) -> Option<String> {
379 match name {
380 "id" => {
381 let entity = NodeRef(handle).entity()?;
382 dom_index_snapshot()
383 .record(entity)
384 .and_then(|r| r.id.clone())
385 }
386 "class" => {
387 let entity = NodeRef(handle).entity()?;
388 let classes = dom_index_snapshot()
389 .record(entity)
390 .map(|r| r.classes.clone())?;
391 if classes.is_empty() {
392 None
393 } else {
394 Some(classes.join(" "))
395 }
396 }
397 "text" => node_text(handle),
398 _ => dom_details_snapshot().detail(handle).and_then(|d| {
399 d.attributes
400 .iter()
401 .find(|(k, _)| k == name)
402 .map(|(_, v)| v.clone())
403 }),
404 }
405}
406
407pub fn attributes_of_handle(handle: u64) -> Vec<(String, String)> {
410 dom_details_snapshot().attributes_of(handle)
411}
412
413pub fn node_id(handle: u64) -> Option<String> {
415 node_get_attr(handle, "id")
416}
417
418pub fn node_class_contains(handle: u64, class: &str) -> bool {
420 let Some(entity) = NodeRef(handle).entity() else {
421 return false;
422 };
423 dom_index_snapshot()
424 .record(entity)
425 .map(|r| r.classes.iter().any(|c| c == class))
426 .unwrap_or(false)
427}
428
429pub fn node_style_get(handle: u64, property: &str) -> Option<String> {
431 dom_details_snapshot().detail(handle).and_then(|d| {
432 d.inline_style
433 .iter()
434 .find(|(k, _)| k == property)
435 .map(|(_, v)| v.clone())
436 })
437}
438
439pub fn node_computed_style(handle: u64, property: &str) -> Option<String> {
444 lumen_ir::css::computed_property(&resolved_attributes(handle), property)
445}
446
447pub fn resolved_attributes(handle: u64) -> lumen_ir::layout_ir::Attributes {
454 let details = dom_details_snapshot();
455 let declarations = |handle: u64| -> Vec<(String, String)> {
458 details
459 .detail(handle)
460 .map(|d| {
461 d.markup_style
462 .iter()
463 .chain(&d.inline_style)
464 .cloned()
465 .collect()
466 })
467 .unwrap_or_default()
468 };
469 let resolved = NodeRef(handle).entity().and_then(|entity| {
470 let sheet = details.sheet.as_ref()?;
471 let index = dom_index_snapshot();
472 let rec = index.record(entity)?;
473 let subject = subject_info(rec).with_declarations(declarations(handle));
474 let mut ancestors: Vec<AncestorInfo> = index
477 .ancestors(entity)
478 .into_iter()
479 .filter_map(|e| {
480 let info = subject_info(index.record(e)?);
481 Some(info.with_declarations(declarations(NodeHandle::new(e).pack())))
482 })
483 .collect();
484 ancestors.reverse();
485 Some(lumen_ir::css::resolve_element(
486 &subject,
487 sheet,
488 &details.media,
489 &ancestors,
490 ))
491 });
492 resolved.unwrap_or_else(|| {
495 let mut attrs = lumen_ir::layout_ir::Attributes::default();
496 for (prop, value) in declarations(handle) {
497 let _ = lumen_ir::css::apply_inline_declaration(&prop, &value, &mut attrs);
498 }
499 attrs
500 })
501}
502
503static FOCUS_STATE: OnceLock<RwLock<(Option<u64>, Option<u64>)>> = OnceLock::new();
510
511fn focus_cell() -> &'static RwLock<(Option<u64>, Option<u64>)> {
512 FOCUS_STATE.get_or_init(|| RwLock::new((None, None)))
513}
514
515pub fn publish_focus(focused: Option<u64>, hovered: Option<u64>) {
518 if let Ok(mut g) = focus_cell().write() {
519 *g = (focused, hovered);
520 }
521}
522
523pub fn focused_node() -> Option<u64> {
525 focus_cell().read().ok().and_then(|g| g.0)
526}
527
528pub fn hovered_node() -> Option<u64> {
530 focus_cell().read().ok().and_then(|g| g.1)
531}
532
533static DOM_CMD_QUEUE: OnceLock<Mutex<Vec<ScriptCommand>>> = OnceLock::new();
542
543fn dom_cmd_queue() -> &'static Mutex<Vec<ScriptCommand>> {
544 DOM_CMD_QUEUE.get_or_init(|| Mutex::new(Vec::new()))
545}
546
547pub fn push_external_dom_command(cmd: ScriptCommand) -> bool {
550 match dom_cmd_queue().lock() {
551 Ok(mut q) => {
552 q.push(cmd);
553 true
554 }
555 Err(_) => false,
556 }
557}
558
559pub fn drain_external_dom_commands() -> Vec<ScriptCommand> {
562 dom_cmd_queue()
563 .lock()
564 .map(|mut q| std::mem::take(&mut *q))
565 .unwrap_or_default()
566}
567
568pub fn build_spawn(tag: &str) -> (u64, ScriptCommand) {
578 let reserved = lumen_core::node::reserve_node_token();
579 (
580 reserved,
581 ScriptCommand::Spawn {
582 tag: tag.to_string(),
583 reserved,
584 },
585 )
586}
587
588pub fn build_clone(source: u64) -> (u64, ScriptCommand) {
591 let reserved = lumen_core::node::reserve_node_token();
592 (reserved, ScriptCommand::CloneNode { source, reserved })
593}
594
595pub fn build_spawn_fragment(
599 key: &str,
600 args: Vec<(String, String)>,
601 children: Vec<(String, u64)>,
602) -> (u64, ScriptCommand) {
603 let reserved = lumen_core::node::reserve_node_token();
604 (
605 reserved,
606 ScriptCommand::SpawnFragment {
607 key: key.to_string(),
608 args,
609 children,
610 reserved,
611 },
612 )
613}
614
615#[cfg(test)]
616mod tests {
617 use super::*;
618 use bevy_ecs::world::World;
619 use lumen_core::node::{DomRecord, publish_dom_index};
620
621 fn rec(
622 entity: Entity,
623 tag: &str,
624 id: Option<&str>,
625 classes: &[&str],
626 parent: Option<Entity>,
627 children: &[Entity],
628 ) -> DomRecord {
629 DomRecord {
630 entity,
631 generation: entity.generation().to_bits(),
632 tag: tag.to_string(),
633 id: id.map(str::to_string),
634 classes: classes.iter().map(|s| s.to_string()).collect(),
635 parent,
636 children: children.to_vec(),
637 child_index: 0,
638 sibling_count: 0,
639 doc_order: 0,
640 }
641 }
642
643 fn fixture() -> (DomIndex, Entity, Entity, Entity, Entity) {
644 let mut w = World::new();
645 let root = w.spawn_empty().id();
646 let card = w.spawn_empty().id();
647 let save = w.spawn_empty().id();
648 let cancel = w.spawn_empty().id();
649 let idx = DomIndex::build(vec![
650 rec(root, "root", Some("app"), &["app"], None, &[card]),
651 rec(card, "div", None, &["card"], Some(root), &[save, cancel]),
652 rec(save, "button", Some("save"), &["row"], Some(card), &[]),
653 rec(cancel, "button", Some("cancel"), &["row"], Some(card), &[]),
654 ]);
655 (idx, root, card, save, cancel)
656 }
657
658 #[test]
659 fn query_by_id_class_and_descendant() {
660 let (idx, _root, _card, save, cancel) = fixture();
661 assert_eq!(query_entities(&idx, "#save").unwrap(), vec![save]);
662 assert_eq!(query_entities(&idx, ".row").unwrap(), vec![save, cancel]);
663 assert_eq!(
664 query_entities(&idx, ".card button").unwrap(),
665 vec![save, cancel]
666 );
667 assert_eq!(
668 query_entities(&idx, ".card > .row").unwrap(),
669 vec![save, cancel]
670 );
671 assert!(query_entities(&idx, ".nope").unwrap().is_empty());
672 }
673
674 #[test]
675 fn closest_walks_up() {
676 let (idx, root, card, save, _cancel) = fixture();
677 assert_eq!(closest_entity(&idx, save, ".card").unwrap(), Some(card));
678 assert_eq!(closest_entity(&idx, save, ".app").unwrap(), Some(root));
679 assert_eq!(closest_entity(&idx, save, ".row").unwrap(), Some(save));
680 assert_eq!(closest_entity(&idx, save, ".none").unwrap(), None);
681 }
682
683 #[test]
684 fn snapshot_entry_points_and_liveness() {
685 let _guard = crate::event::TEST_GUARD
688 .lock()
689 .unwrap_or_else(|e| e.into_inner());
690 let (idx, root, card, save, cancel) = fixture();
691 publish_dom_index(idx);
692 let q = run_query(".row").unwrap();
693 assert_eq!(q.len(), 2);
694 assert!(q.single().is_err());
695 let one = run_query("#save").unwrap();
696 assert_eq!(one.single().unwrap(), NodeHandle::new(save).pack());
697 assert_eq!(
698 run_get_by_id("cancel"),
699 Some(NodeHandle::new(cancel).pack())
700 );
701 assert_eq!(run_document(), Some(NodeHandle::new(root).pack()));
702 let save_h = NodeHandle::new(save).pack();
703 assert_eq!(node_next(save_h), Some(NodeHandle::new(cancel).pack()));
704 assert_eq!(node_parent(save_h), Some(NodeHandle::new(card).pack()));
705 assert_eq!(node_children(NodeHandle::new(card).pack()).len(), 2);
706 assert_eq!(
707 node_closest(save_h, ".app").unwrap(),
708 Some(NodeHandle::new(root).pack())
709 );
710 assert!(node_valid(save_h));
711 assert!(!node_valid(0));
712 }
713}