use crate::core::{Element, ElementId, ElementType, NodeKey, Props, VNode, VNodeType};
use crate::layout::measure::measure_text_width;
use crate::reconciler::{Patch, PatchType};
use std::collections::HashMap;
use taffy::{AvailableSpace, NodeId, TaffyTree};
#[derive(Debug, Clone, Copy, Default)]
pub struct Layout {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
}
#[derive(Clone)]
struct NodeContext {
text_content: Option<String>,
}
pub struct LayoutEngine {
taffy: TaffyTree<NodeContext>,
node_map: HashMap<ElementId, NodeId>,
vnode_map: HashMap<NodeKey, NodeId>,
root_node: Option<NodeId>,
last_width: u16,
last_height: u16,
}
impl LayoutEngine {
pub fn new() -> Self {
Self {
taffy: TaffyTree::new(),
node_map: HashMap::new(),
vnode_map: HashMap::new(),
root_node: None,
last_width: 0,
last_height: 0,
}
}
pub fn build_tree(&mut self, element: &Element) -> Option<NodeId> {
self.taffy.clear();
self.node_map.clear();
self.vnode_map.clear();
self.root_node = None;
self.build_node(element)
}
fn build_node(&mut self, element: &Element) -> Option<NodeId> {
if element.element_type == ElementType::VirtualText {
return None;
}
let taffy_style = element.style.to_taffy();
let child_nodes: Vec<NodeId> = element
.children
.iter()
.filter_map(|child| self.build_node(child))
.collect();
let context = NodeContext {
text_content: element.text_content.clone(),
};
let node_id = if element.is_text() {
self.taffy
.new_leaf_with_context(taffy_style, context)
.ok()?
} else {
let node = self
.taffy
.new_with_children(taffy_style, &child_nodes)
.ok()?;
let _ = self.taffy.set_node_context(node, Some(context));
node
};
self.node_map.insert(element.id, node_id);
Some(node_id)
}
pub fn compute(&mut self, root: &Element, width: u16, height: u16) {
if let Some(root_node) = self.build_tree(root) {
self.root_node = Some(root_node);
self.last_width = width;
self.last_height = height;
let _ = self.taffy.compute_layout_with_measure(
root_node,
taffy::Size {
width: AvailableSpace::Definite(width as f32),
height: AvailableSpace::Definite(height as f32),
},
|known_dimensions, available_space, _node_id, node_context, _style| {
measure_text_node(known_dimensions, available_space, node_context)
},
);
}
}
pub fn build_vnode_tree(&mut self, vnode: &VNode) -> Option<NodeId> {
self.taffy.clear();
self.node_map.clear();
self.vnode_map.clear();
self.root_node = self.build_vnode(vnode);
self.root_node
}
fn build_vnode(&mut self, vnode: &VNode) -> Option<NodeId> {
let taffy_style = vnode.props.to_taffy();
let child_nodes: Vec<NodeId> = vnode
.children
.iter()
.filter_map(|child| self.build_vnode(child))
.collect();
let text_content = match &vnode.node_type {
VNodeType::Text(s) => Some(s.clone()),
_ => None,
};
let context = NodeContext {
text_content,
};
let node_id = if vnode.is_text() {
self.taffy
.new_leaf_with_context(taffy_style, context)
.ok()?
} else {
let node = self
.taffy
.new_with_children(taffy_style, &child_nodes)
.ok()?;
let _ = self.taffy.set_node_context(node, Some(context));
node
};
self.vnode_map.insert(vnode.key, node_id);
Some(node_id)
}
pub fn compute_vnode(&mut self, root: &VNode, width: u16, height: u16) {
if let Some(root_node) = self.build_vnode_tree(root) {
self.last_width = width;
self.last_height = height;
let _ = self.taffy.compute_layout_with_measure(
root_node,
taffy::Size {
width: AvailableSpace::Definite(width as f32),
height: AvailableSpace::Definite(height as f32),
},
|known_dimensions, available_space, _node_id, node_context, _style| {
measure_text_node(known_dimensions, available_space, node_context)
},
);
}
}
pub fn apply_patches(&mut self, patches: &[Patch]) -> bool {
if patches.is_empty() {
return false;
}
let mut needs_recompute = false;
for patch in patches {
match patch.patch_type {
PatchType::Create => {
if let (Some(new_node), Some(parent_key)) = (&patch.new_node, patch.parent) {
if self.create_vnode(new_node, parent_key).is_some() {
needs_recompute = true;
}
}
}
PatchType::Update => {
if let Some(new_props) = &patch.new_props {
if self.update_node_props(patch.key, new_props) {
needs_recompute = true;
}
}
}
PatchType::Remove => {
if self.remove_node(patch.key) {
needs_recompute = true;
}
}
PatchType::Replace => {
if let Some(new_node) = &patch.new_node {
if self.replace_node(patch.key, new_node) {
needs_recompute = true;
}
}
}
PatchType::Reorder => {
if self.reorder_children(patch.key, &patch.moves) {
needs_recompute = true;
}
}
}
}
if needs_recompute {
self.recompute_layout();
}
needs_recompute
}
fn create_vnode(&mut self, vnode: &VNode, parent_key: NodeKey) -> Option<NodeId> {
let parent_node = *self.vnode_map.get(&parent_key)?;
let new_node_id = self.build_vnode(vnode)?;
let _ = self.taffy.add_child(parent_node, new_node_id);
Some(new_node_id)
}
fn update_node_props(&mut self, key: NodeKey, props: &Props) -> bool {
if let Some(&node_id) = self.vnode_map.get(&key) {
let new_style = props.to_taffy();
if self.taffy.set_style(node_id, new_style).is_ok() {
return true;
}
}
false
}
fn remove_node(&mut self, key: NodeKey) -> bool {
if let Some(node_id) = self.vnode_map.remove(&key) {
if self.taffy.remove(node_id).is_ok() {
return true;
}
}
false
}
fn replace_node(&mut self, old_key: NodeKey, new_node: &VNode) -> bool {
if let Some(&old_node_id) = self.vnode_map.get(&old_key) {
if let Some(parent_id) = self.taffy.parent(old_node_id) {
let children: Vec<_> = self.taffy.children(parent_id).unwrap_or_default();
let index = children.iter().position(|&id| id == old_node_id);
let _ = self.taffy.remove(old_node_id);
self.vnode_map.remove(&old_key);
if let Some(new_node_id) = self.build_vnode(new_node) {
if let Some(idx) = index {
let _ = self
.taffy
.insert_child_at_index(parent_id, idx, new_node_id);
} else {
let _ = self.taffy.add_child(parent_id, new_node_id);
}
return true;
}
}
}
false
}
fn reorder_children(&mut self, parent_key: NodeKey, moves: &[(usize, usize)]) -> bool {
if moves.is_empty() {
return false;
}
if let Some(&parent_id) = self.vnode_map.get(&parent_key) {
let mut children: Vec<_> = self.taffy.children(parent_id).unwrap_or_default();
for &(from, to) in moves {
if from < children.len() && to < children.len() {
let child = children.remove(from);
children.insert(to, child);
}
}
if self.taffy.set_children(parent_id, &children).is_ok() {
return true;
}
}
false
}
fn recompute_layout(&mut self) {
if let Some(root_node) = self.root_node {
let _ = self.taffy.compute_layout_with_measure(
root_node,
taffy::Size {
width: AvailableSpace::Definite(self.last_width as f32),
height: AvailableSpace::Definite(self.last_height as f32),
},
|known_dimensions, available_space, _node_id, node_context, _style| {
measure_text_node(known_dimensions, available_space, node_context)
},
);
}
}
pub fn get_layout(&self, element_id: ElementId) -> Option<Layout> {
let node_id = self.node_map.get(&element_id)?;
let layout = self.taffy.layout(*node_id).ok()?;
Some(Layout {
x: layout.location.x,
y: layout.location.y,
width: layout.size.width,
height: layout.size.height,
})
}
pub fn get_vnode_layout(&self, key: NodeKey) -> Option<Layout> {
let node_id = self.vnode_map.get(&key)?;
let layout = self.taffy.layout(*node_id).ok()?;
Some(Layout {
x: layout.location.x,
y: layout.location.y,
width: layout.size.width,
height: layout.size.height,
})
}
pub fn get_all_layouts(&self) -> HashMap<ElementId, Layout> {
self.node_map
.iter()
.filter_map(|(element_id, node_id)| {
let layout = self.taffy.layout(*node_id).ok()?;
Some((
*element_id,
Layout {
x: layout.location.x,
y: layout.location.y,
width: layout.size.width,
height: layout.size.height,
},
))
})
.collect()
}
pub fn get_all_vnode_layouts(&self) -> HashMap<NodeKey, Layout> {
self.vnode_map
.iter()
.filter_map(|(key, node_id)| {
let layout = self.taffy.layout(*node_id).ok()?;
Some((
*key,
Layout {
x: layout.location.x,
y: layout.location.y,
width: layout.size.width,
height: layout.size.height,
},
))
})
.collect()
}
pub fn has_tree(&self) -> bool {
self.root_node.is_some()
}
pub fn node_count(&self) -> usize {
self.node_map.len() + self.vnode_map.len()
}
}
impl Default for LayoutEngine {
fn default() -> Self {
Self::new()
}
}
fn measure_text_node(
known_dimensions: taffy::Size<Option<f32>>,
available_space: taffy::Size<AvailableSpace>,
node_context: Option<&mut NodeContext>,
) -> taffy::Size<f32> {
let text = node_context
.and_then(|ctx| ctx.text_content.as_ref())
.map(|s| s.as_str())
.unwrap_or("");
if text.is_empty() {
return taffy::Size {
width: known_dimensions.width.unwrap_or(0.0),
height: known_dimensions.height.unwrap_or(0.0),
};
}
let text_width = measure_text_width(text) as f32;
let available_width = match available_space.width {
AvailableSpace::Definite(w) => Some(w as usize),
_ => None,
};
let text_height = if let Some(max_width) = available_width {
if max_width > 0 && text_width > max_width as f32 {
use super::measure::wrap_text;
let wrapped = wrap_text(text, max_width);
wrapped.lines().count().max(1) as f32
} else {
text.lines().count().max(1) as f32
}
} else {
text.lines().count().max(1) as f32
};
let width = known_dimensions
.width
.unwrap_or_else(|| match available_space.width {
AvailableSpace::Definite(w) => text_width.min(w),
AvailableSpace::MinContent => text_width,
AvailableSpace::MaxContent => text_width,
});
let height = known_dimensions.height.unwrap_or(text_height);
taffy::Size { width, height }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Element, Props, Style, VNode};
use crate::reconciler::Patch;
#[test]
fn test_layout_engine_creation() {
let engine = LayoutEngine::new();
assert!(engine.node_map.is_empty());
assert!(engine.vnode_map.is_empty());
assert!(!engine.has_tree());
}
#[test]
fn test_simple_layout() {
let mut engine = LayoutEngine::new();
let mut root = Element::root();
root.add_child(Element::text("Hello"));
engine.compute(&root, 80, 24);
let layout = engine.get_layout(root.id);
assert!(layout.is_some());
}
#[test]
fn test_text_measurement() {
let mut engine = LayoutEngine::new();
let root = Element::text("Hello World");
engine.compute(&root, 80, 24);
let layout = engine.get_layout(root.id);
assert!(layout.is_some());
let layout = layout.unwrap();
assert!(layout.width >= 11.0);
}
#[test]
fn test_vnode_layout() {
let mut engine = LayoutEngine::new();
let root = VNode::box_node()
.child(VNode::text("Hello"))
.child(VNode::text("World"));
engine.compute_vnode(&root, 80, 24);
assert!(engine.has_tree());
let layout = engine.get_vnode_layout(root.key);
assert!(layout.is_some());
}
#[test]
fn test_vnode_text_measurement() {
let mut engine = LayoutEngine::new();
let root = VNode::text("Hello World");
engine.compute_vnode(&root, 80, 24);
let layout = engine.get_vnode_layout(root.key);
assert!(layout.is_some());
let layout = layout.unwrap();
assert!(layout.width >= 11.0);
}
#[test]
fn test_apply_patches_update() {
let mut engine = LayoutEngine::new();
let root = VNode::box_node().child(VNode::text("Hello"));
engine.compute_vnode(&root, 80, 24);
let mut new_style = Style::new();
new_style.padding.top = 5.0;
let new_props = Props::with_style(new_style);
let patches = vec![Patch::update(root.key, Props::new(), new_props)];
let changed = engine.apply_patches(&patches);
assert!(changed);
}
#[test]
fn test_apply_patches_empty() {
let mut engine = LayoutEngine::new();
let root = VNode::box_node();
engine.compute_vnode(&root, 80, 24);
let changed = engine.apply_patches(&[]);
assert!(!changed);
}
#[test]
fn test_apply_patches_create() {
let mut engine = LayoutEngine::new();
let root = VNode::box_node();
engine.compute_vnode(&root, 80, 24);
let new_child = VNode::text("New child");
let patches = vec![Patch::create(new_child, root.key)];
let changed = engine.apply_patches(&patches);
assert!(changed);
}
#[test]
fn test_apply_patches_remove() {
let mut engine = LayoutEngine::new();
let child = VNode::text("Child");
let child_key = child.key;
let root = VNode::box_node().child(child);
engine.compute_vnode(&root, 80, 24);
let patches = vec![Patch::remove(child_key)];
let changed = engine.apply_patches(&patches);
assert!(changed);
assert!(engine.get_vnode_layout(child_key).is_none());
}
#[test]
fn test_get_all_vnode_layouts() {
let mut engine = LayoutEngine::new();
let root = VNode::box_node()
.child(VNode::text("A"))
.child(VNode::text("B"));
engine.compute_vnode(&root, 80, 24);
let layouts = engine.get_all_vnode_layouts();
assert_eq!(layouts.len(), 3); }
#[test]
fn test_node_count() {
let mut engine = LayoutEngine::new();
let root = VNode::box_node()
.child(VNode::text("A").with_key("a"))
.child(VNode::box_node().child(VNode::text("B").with_key("b")));
engine.compute_vnode(&root, 80, 24);
assert_eq!(engine.node_count(), 4);
}
}