1use bevy_ecs::entity::Entity;
19use lumen_core::node::{DomIndex, NodeHandle, dom_index_snapshot};
20use std::collections::HashMap;
21use std::sync::{Arc, OnceLock, RwLock};
22
23use crate::node_query::{
24 NodeRef, ancestor_infos, attributes_of_handle, dom_details_snapshot, resolved_attributes,
25};
26
27#[derive(Debug, Clone, Copy, Default, PartialEq)]
34pub struct NodeRect {
35 pub x: f32,
37 pub y: f32,
39 pub width: f32,
41 pub height: f32,
43 pub client_x: f32,
45 pub client_y: f32,
47}
48
49#[derive(Debug, Clone, Copy, Default, PartialEq)]
51pub struct NodeScroll {
52 pub x: f32,
54 pub y: f32,
56 pub max_x: f32,
58 pub max_y: f32,
60}
61
62#[derive(Debug, Clone, Copy, Default)]
64pub struct NodeGeometry {
65 pub rect: NodeRect,
67 pub content_rect: NodeRect,
69 pub scroll: NodeScroll,
71 pub visible: bool,
73 pub z_index: i32,
75}
76
77#[derive(Debug, Clone, Copy, Default)]
79pub struct PointerSnapshot {
80 pub x: f32,
82 pub y: f32,
84 pub inside: bool,
86 pub buttons: u32,
88 pub shift: bool,
90 pub ctrl: bool,
92 pub alt: bool,
94 pub super_: bool,
96}
97
98#[derive(Debug, Clone, Copy, Default)]
100pub struct FrameInfo {
101 pub frame: u64,
103 pub dt_ms: f64,
105 pub dirty_count: u64,
107}
108
109#[derive(Debug, Clone)]
112pub struct MatchedRuleView {
113 pub selector: String,
115 pub specificity: (u32, u32, u32),
117 pub source: String,
119 pub source_order: usize,
121 pub declarations: Vec<(String, String)>,
123}
124
125pub type ComponentFields = Vec<(String, String)>;
131pub type ComponentMaps = HashMap<u64, Vec<(String, ComponentFields)>>;
134
135#[derive(Default)]
137pub struct IntrospectSnapshot {
138 geometry: HashMap<u64, NodeGeometry>,
139 components: ComponentMaps,
140 known_components: Vec<String>,
141 frame: FrameInfo,
142 signals: Vec<(String, String)>,
143}
144
145impl IntrospectSnapshot {
146 pub fn new(
148 geometry: HashMap<u64, NodeGeometry>,
149 components: ComponentMaps,
150 known_components: Vec<String>,
151 frame: FrameInfo,
152 signals: Vec<(String, String)>,
153 ) -> Self {
154 Self {
155 geometry,
156 components,
157 known_components,
158 frame,
159 signals,
160 }
161 }
162}
163
164static INTROSPECT: OnceLock<RwLock<Arc<IntrospectSnapshot>>> = OnceLock::new();
165
166fn cell() -> &'static RwLock<Arc<IntrospectSnapshot>> {
167 INTROSPECT.get_or_init(|| RwLock::new(Arc::new(IntrospectSnapshot::default())))
168}
169
170pub fn publish_introspection(snapshot: IntrospectSnapshot) {
172 if let Ok(mut g) = cell().write() {
173 *g = Arc::new(snapshot);
174 }
175}
176
177fn snapshot() -> Arc<IntrospectSnapshot> {
178 cell()
179 .read()
180 .map(|g| g.clone())
181 .unwrap_or_else(|_| Arc::new(IntrospectSnapshot::default()))
182}
183
184pub fn node_rect(handle: u64) -> Option<NodeRect> {
190 snapshot().geometry.get(&handle).map(|g| g.rect)
191}
192
193pub fn node_content_rect(handle: u64) -> Option<NodeRect> {
195 snapshot().geometry.get(&handle).map(|g| g.content_rect)
196}
197
198pub fn node_scroll(handle: u64) -> Option<NodeScroll> {
200 snapshot().geometry.get(&handle).map(|g| g.scroll)
201}
202
203pub fn node_is_visible(handle: u64) -> bool {
206 snapshot()
207 .geometry
208 .get(&handle)
209 .map(|g| g.visible)
210 .unwrap_or(false)
211}
212
213pub fn node_z_index(handle: u64) -> i32 {
215 snapshot()
216 .geometry
217 .get(&handle)
218 .map(|g| g.z_index)
219 .unwrap_or(0)
220}
221
222pub fn node_computed_style_map(handle: u64) -> Vec<(String, String)> {
230 lumen_ir::css::computed_style_map(&resolved_attributes(handle))
231}
232
233pub fn node_matched_rules(handle: u64) -> Vec<MatchedRuleView> {
236 let Some(entity) = NodeRef(handle).entity() else {
237 return Vec::new();
238 };
239 let details = dom_details_snapshot();
240 let Some(sheet) = details.sheet() else {
241 return Vec::new();
242 };
243 let index = dom_index_snapshot();
244 let Some(rec) = index.record(entity) else {
245 return Vec::new();
246 };
247 let subject =
248 lumen_ir::css::AncestorInfo::new(rec.tag.clone(), rec.classes.clone(), rec.id.clone())
249 .with_position(crate::node_query::record_position(rec));
250 let ancestors = ancestor_infos(&index, entity);
251 let has_children = !rec.children.is_empty();
252 let text = crate::node_query::node_text(handle);
253 lumen_ir::css::matched_rules_for(
254 &subject,
255 sheet,
256 &details.media(),
257 &ancestors,
258 has_children,
259 text.as_deref(),
260 )
261 .into_iter()
262 .map(|m| MatchedRuleView {
263 selector: m.selector,
264 specificity: (m.specificity.a, m.specificity.b, m.specificity.c),
265 source: match m.origin {
266 lumen_ir::css::Origin::UserAgent => "user-agent".to_string(),
267 lumen_ir::css::Origin::Author => "author".to_string(),
268 },
269 source_order: m.source_order,
270 declarations: m.declarations,
271 })
272 .collect()
273}
274
275pub fn node_inline_style(handle: u64) -> Vec<(String, String)> {
277 dom_details_snapshot().inline_style_of(handle)
278}
279
280pub fn node_attrs(handle: u64) -> Vec<(String, String)> {
283 let mut out: Vec<(String, String)> = Vec::new();
284 let index = dom_index_snapshot();
285 if let Some(entity) = NodeRef(handle).entity()
286 && let Some(rec) = index.record(entity)
287 {
288 if let Some(id) = &rec.id {
289 out.push(("id".to_string(), id.clone()));
290 }
291 if !rec.classes.is_empty() {
292 out.push(("class".to_string(), rec.classes.join(" ")));
293 }
294 }
295 if let Some(text) = crate::node_query::node_text(handle) {
296 out.push(("text".to_string(), text));
297 }
298 out.extend(attributes_of_handle(handle));
299 out
300}
301
302pub fn node_classes(handle: u64) -> Vec<String> {
304 let Some(entity) = NodeRef(handle).entity() else {
305 return Vec::new();
306 };
307 dom_index_snapshot()
308 .record(entity)
309 .map(|r| r.classes.clone())
310 .unwrap_or_default()
311}
312
313pub fn node_entity_id(handle: u64) -> Option<(u32, u32)> {
320 let h = NodeHandle::unpack(handle)?;
321 let bits = h.entity.to_bits();
324 Some((bits as u32, (bits >> 32) as u32))
325}
326
327pub fn node_components(handle: u64) -> Vec<String> {
330 snapshot()
331 .components
332 .get(&handle)
333 .map(|list| list.iter().map(|(n, _)| n.clone()).collect())
334 .unwrap_or_default()
335}
336
337pub fn node_component(handle: u64, name: &str) -> Result<Option<Vec<(String, String)>>, String> {
341 let snap = snapshot();
342 if !snap.known_components.iter().any(|n| n == name) {
343 return Err(format!(
344 "component(\"{name}\"): not a whitelisted component"
345 ));
346 }
347 Ok(snap.components.get(&handle).and_then(|list| {
348 list.iter()
349 .find(|(n, _)| n == name)
350 .map(|(_, map)| map.clone())
351 }))
352}
353
354pub fn outer_markup(handle: u64) -> String {
360 let Some(entity) = NodeRef(handle).entity() else {
361 return String::new();
362 };
363 let index = dom_index_snapshot();
364 let mut out = String::new();
365 write_markup(&index, entity, 0, &mut out);
366 out
367}
368
369pub fn inner_markup(handle: u64) -> String {
373 let Some(entity) = NodeRef(handle).entity() else {
374 return String::new();
375 };
376 let index = dom_index_snapshot();
377 let Some(rec) = index.record(entity) else {
378 return String::new();
379 };
380 let mut out = String::new();
381 for child in &rec.children {
382 write_markup(&index, *child, 0, &mut out);
383 }
384 out
385}
386
387fn write_markup(index: &DomIndex, entity: Entity, depth: usize, out: &mut String) {
388 let Some(rec) = index.record(entity) else {
389 return;
390 };
391 let pad = " ".repeat(depth);
392 let tag = if rec.tag.is_empty() { "div" } else { &rec.tag };
393 out.push_str(&pad);
394 out.push('<');
395 out.push_str(tag);
396 if let Some(id) = &rec.id {
397 out.push_str(&format!(" id=\"{id}\""));
398 }
399 if !rec.classes.is_empty() {
400 out.push_str(&format!(" class=\"{}\"", rec.classes.join(" ")));
401 }
402 let handle = NodeHandle::new(entity).pack();
403 for (k, v) in attributes_of_handle(handle) {
404 out.push_str(&format!(" {k}=\"{v}\""));
405 }
406 let text = crate::node_query::node_text(handle);
407 if rec.children.is_empty() && text.as_deref().unwrap_or("").is_empty() {
408 out.push_str("/>\n");
409 return;
410 }
411 out.push('>');
412 if let Some(t) = &text
413 && !t.is_empty()
414 {
415 out.push_str(t);
416 }
417 if rec.children.is_empty() {
418 out.push_str(&format!("</{tag}>\n"));
419 return;
420 }
421 out.push('\n');
422 for child in &rec.children {
423 write_markup(index, *child, depth + 1, out);
424 }
425 out.push_str(&pad);
426 out.push_str(&format!("</{tag}>\n"));
427}
428
429pub fn dump_tree() -> String {
432 let index = dom_index_snapshot();
433 let snap = snapshot();
434 let mut out = String::new();
435 for root in index.roots() {
436 write_dump(&index, &snap, *root, 0, &mut out);
437 }
438 out
439}
440
441fn write_dump(
442 index: &DomIndex,
443 snap: &IntrospectSnapshot,
444 entity: Entity,
445 depth: usize,
446 out: &mut String,
447) {
448 let Some(rec) = index.record(entity) else {
449 return;
450 };
451 let pad = " ".repeat(depth);
452 let tag = if rec.tag.is_empty() { "div" } else { &rec.tag };
453 out.push_str(&pad);
454 out.push_str(tag);
455 if let Some(id) = &rec.id {
456 out.push('#');
457 out.push_str(id);
458 }
459 for c in &rec.classes {
460 out.push('.');
461 out.push_str(c);
462 }
463 let handle = NodeHandle::new(entity).pack();
464 if let Some(g) = snap.geometry.get(&handle) {
465 out.push_str(&format!(
466 " [{} {} {} {}]",
467 g.rect.client_x, g.rect.client_y, g.rect.width, g.rect.height
468 ));
469 }
470 out.push('\n');
471 for child in &rec.children {
472 write_dump(index, snap, *child, depth + 1, out);
473 }
474}
475
476pub fn pointer_state() -> PointerSnapshot {
482 pointer_cell().read().map(|g| *g).unwrap_or_default()
483}
484
485static POINTER: OnceLock<RwLock<PointerSnapshot>> = OnceLock::new();
486
487fn pointer_cell() -> &'static RwLock<PointerSnapshot> {
488 POINTER.get_or_init(|| RwLock::new(PointerSnapshot::default()))
489}
490
491pub fn publish_pointer(pointer: PointerSnapshot) {
493 if let Ok(mut g) = pointer_cell().write() {
494 *g = pointer;
495 }
496}
497
498pub fn publish_pointer_state(
504 state: Option<bevy_ecs::system::Res<lumen_core::input::PointerState>>,
505 modifiers: Option<bevy_ecs::system::Res<lumen_core::input::ModifiersState>>,
506) {
507 let state = state.map(|s| *s).unwrap_or_default();
508 let m = modifiers.map(|m| m.0).unwrap_or_default();
509 let position = state.position.unwrap_or(glam::Vec2::ZERO);
510 publish_pointer(PointerSnapshot {
511 x: position.x,
512 y: position.y,
513 inside: state.position.is_some(),
514 buttons: u32::from(state.primary_down),
515 shift: m.shift,
516 ctrl: m.ctrl,
517 alt: m.alt,
518 super_: m.super_,
519 });
520}
521
522pub fn frame_info() -> FrameInfo {
524 snapshot().frame
525}
526
527pub fn signals_all() -> Vec<(String, String)> {
530 snapshot().signals.clone()
531}