use anyhow::Result;
use std::sync::{Arc, Mutex};
use super::AssetDB;
pub trait LayoutBackend: Send + Sync {
fn hydrate(&self, db: &Arc<AssetDB>) -> Result<()>;
fn sync(&self, db: &Arc<AssetDB>) -> Result<()>;
fn poll_events(&self, db: &Arc<AssetDB>) -> Result<()>;
fn query(&self, entity: &str, property: &str) -> Option<f64>;
fn query_string(&self, entity: &str, property: &str) -> Option<String>;
fn hit_test(&self, x: f64, y: f64) -> Option<String>;
fn parent_of(&self, entity: &str) -> Option<String>;
fn children_of(&self, entity: &str) -> Option<Vec<String>>;
fn backend_name(&self) -> &'static str;
}
pub struct HeadlessLayoutBackend {
nodes: std::sync::RwLock<std::collections::HashMap<String, LayoutNode>>,
}
#[derive(Default, Clone)]
pub struct LayoutNode {
pub tag: String,
pub text: String,
pub parent: Option<String>,
pub children: Vec<String>,
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
pub scroll_x: f64,
pub scroll_y: f64,
pub scroll_height: f64,
pub opacity: f64,
pub props: std::collections::HashMap<String, f64>,
pub string_props: std::collections::HashMap<String, String>,
}
impl Default for HeadlessLayoutBackend {
fn default() -> Self {
Self::new()
}
}
impl HeadlessLayoutBackend {
pub fn new() -> Self {
Self {
nodes: std::sync::RwLock::new(std::collections::HashMap::new()),
}
}
pub fn set_node(&self, entity: &str, node: LayoutNode) {
if let Ok(mut nodes) = self.nodes.write() {
nodes.insert(entity.to_string(), node);
}
}
pub fn remove_node(&self, entity: &str) {
if let Ok(mut nodes) = self.nodes.write() {
nodes.remove(entity);
}
}
}
impl LayoutBackend for HeadlessLayoutBackend {
fn hydrate(&self, db: &Arc<AssetDB>) -> Result<()> {
let dom_entities = db.entities_with(&["dom"])?;
let mut nodes = self.nodes.write().map_err(|e| anyhow::anyhow!("{}", e))?;
for entity in &dom_entities {
if let Ok(asset) = db.get_component(entity, "dom") {
let v: serde_json::Value = if let Some(ref inline) = asset.entry.inline_data {
inline.clone()
} else {
serde_json::from_slice(&asset.data).unwrap_or_default()
};
let mut node = LayoutNode {
tag: v
.get("tag")
.and_then(|v| v.as_str())
.unwrap_or("div")
.to_string(),
text: v
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
parent: v
.get("parent")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
width: v.get("width").and_then(|v| v.as_f64()).unwrap_or(0.0),
height: v.get("height").and_then(|v| v.as_f64()).unwrap_or(0.0),
opacity: 1.0,
..Default::default()
};
if let Ok(tf) = db.get_component(entity, "transform") {
let tv: serde_json::Value = if let Some(ref inline) = tf.entry.inline_data {
inline.clone()
} else {
serde_json::from_slice(&tf.data).unwrap_or_default()
};
if let Some(pos) = tv.get("position").and_then(|v| v.as_array()) {
node.x = pos.first().and_then(|v| v.as_f64()).unwrap_or(0.0);
node.y = pos.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0);
}
}
nodes.insert(entity.clone(), node);
}
}
Ok(())
}
fn sync(&self, db: &Arc<AssetDB>) -> Result<()> {
let nodes_to_sync: Vec<String> = {
let nodes = self.nodes.read().map_err(|e| anyhow::anyhow!("{}", e))?;
nodes.keys().cloned().collect()
};
let mut nodes = self.nodes.write().map_err(|e| anyhow::anyhow!("{}", e))?;
for entity in &nodes_to_sync {
if let Ok(tf) = db.get_component(entity, "transform") {
let tv: serde_json::Value = if let Some(ref inline) = tf.entry.inline_data {
inline.clone()
} else {
serde_json::from_slice(&tf.data).unwrap_or_default()
};
if let Some(node) = nodes.get_mut(entity) {
if let Some(pos) = tv.get("position").and_then(|v| v.as_array()) {
node.x = pos.first().and_then(|v| v.as_f64()).unwrap_or(node.x);
node.y = pos.get(1).and_then(|v| v.as_f64()).unwrap_or(node.y);
}
}
}
if let Ok(style) = db.get_component(entity, "style") {
let sv: serde_json::Value = if let Some(ref inline) = style.entry.inline_data {
inline.clone()
} else {
serde_json::from_slice(&style.data).unwrap_or_default()
};
if let Some(node) = nodes.get_mut(entity) {
if let Some(o) = sv.get("opacity").and_then(|v| v.as_f64()) {
node.opacity = o;
}
}
}
}
Ok(())
}
fn poll_events(&self, _db: &Arc<AssetDB>) -> Result<()> {
Ok(())
}
fn query(&self, entity: &str, property: &str) -> Option<f64> {
let nodes = self.nodes.read().ok()?;
let node = nodes.get(entity)?;
match property {
"x" => Some(node.x),
"y" => Some(node.y),
"width" => Some(node.width),
"height" => Some(node.height),
"scrollX" => Some(node.scroll_x),
"scrollY" => Some(node.scroll_y),
"scrollProgress" => {
if node.scroll_height > node.height {
Some(node.scroll_y / (node.scroll_height - node.height))
} else {
Some(0.0)
}
}
"inViewport" => Some(1.0), "opacity" => Some(node.opacity),
_ => node.props.get(property).copied(),
}
}
fn query_string(&self, entity: &str, property: &str) -> Option<String> {
let nodes = self.nodes.read().ok()?;
let node = nodes.get(entity)?;
match property {
"tag" => Some(node.tag.clone()),
"text" => Some(node.text.clone()),
_ => node.string_props.get(property).cloned(),
}
}
fn hit_test(&self, x: f64, y: f64) -> Option<String> {
let nodes = self.nodes.read().ok()?;
let mut hit = None;
for (entity, node) in nodes.iter() {
if x >= node.x && x <= node.x + node.width && y >= node.y && y <= node.y + node.height {
hit = Some(entity.clone());
}
}
hit
}
fn parent_of(&self, entity: &str) -> Option<String> {
let nodes = self.nodes.read().ok()?;
nodes.get(entity)?.parent.clone()
}
fn children_of(&self, entity: &str) -> Option<Vec<String>> {
let nodes = self.nodes.read().ok()?;
Some(nodes.get(entity)?.children.clone())
}
fn backend_name(&self) -> &'static str {
"headless"
}
}
static LAYOUT_REGISTRY: std::sync::OnceLock<
Mutex<std::collections::HashMap<String, Arc<dyn LayoutBackend>>>,
> = std::sync::OnceLock::new();
fn layout_registry() -> &'static Mutex<std::collections::HashMap<String, Arc<dyn LayoutBackend>>> {
LAYOUT_REGISTRY.get_or_init(|| Mutex::new(std::collections::HashMap::new()))
}
pub fn set_layout_backend(db_path: &str, backend: Arc<dyn LayoutBackend>) {
if let Ok(mut reg) = layout_registry().lock() {
reg.insert(db_path.to_string(), backend);
}
}
pub fn get_layout_backend(db_path: &str) -> Option<Arc<dyn LayoutBackend>> {
layout_registry().lock().ok()?.get(db_path).cloned()
}
pub fn resolve_layout_query(db_path: &str, query: &str) -> Option<f64> {
let backend = get_layout_backend(db_path)?;
let colon = query.rfind(':')?;
let entity = &query[..colon];
let property = &query[colon + 1..];
backend.query(entity, property)
}