cssbox-core 0.1.0

Standalone CSS layout engine — core algorithms
Documentation
//! Box tree representation for layout.
//!
//! The box tree is the input to the layout engine. It represents the CSS box model
//! structure after display computation has determined which formatting contexts apply.

use crate::style::{ComputedStyle, DisplayInner};

/// A unique identifier for a node in the box tree.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(pub usize);

impl NodeId {
    pub const ROOT: Self = NodeId(0);
}

/// Text content for inline layout.
#[derive(Debug, Clone)]
pub struct TextContent {
    pub text: String,
}

/// The kind of content a box tree node holds.
#[derive(Debug, Clone)]
pub enum NodeKind {
    /// An element node with children.
    Element,
    /// A text node (leaf).
    Text(TextContent),
    /// An anonymous block wrapper (generated by the engine).
    AnonymousBlock,
    /// An anonymous inline wrapper.
    AnonymousInline,
}

/// A single node in the box tree.
#[derive(Debug, Clone)]
pub struct BoxTreeNode {
    pub id: NodeId,
    pub kind: NodeKind,
    pub style: ComputedStyle,
    pub children: Vec<NodeId>,
    pub parent: Option<NodeId>,
}

impl BoxTreeNode {
    pub fn new(id: NodeId, kind: NodeKind, style: ComputedStyle) -> Self {
        Self {
            id,
            kind,
            style,
            children: Vec::new(),
            parent: None,
        }
    }

    pub fn is_text(&self) -> bool {
        matches!(self.kind, NodeKind::Text(_))
    }

    pub fn text_content(&self) -> Option<&str> {
        match &self.kind {
            NodeKind::Text(tc) => Some(&tc.text),
            _ => None,
        }
    }
}

/// The box tree — arena-allocated tree of layout boxes.
#[derive(Debug, Clone)]
pub struct BoxTree {
    nodes: Vec<BoxTreeNode>,
    root: NodeId,
}

impl BoxTree {
    pub fn new() -> Self {
        Self {
            nodes: Vec::new(),
            root: NodeId(0),
        }
    }

    /// Add a new node to the tree. Returns the node's ID.
    pub fn add_node(&mut self, kind: NodeKind, style: ComputedStyle) -> NodeId {
        let id = NodeId(self.nodes.len());
        self.nodes.push(BoxTreeNode::new(id, kind, style));
        id
    }

    /// Set the root node.
    pub fn set_root(&mut self, id: NodeId) {
        self.root = id;
    }

    /// Append a child to a parent node.
    pub fn append_child(&mut self, parent: NodeId, child: NodeId) {
        self.nodes[child.0].parent = Some(parent);
        self.nodes[parent.0].children.push(child);
    }

    /// Get a reference to a node.
    pub fn node(&self, id: NodeId) -> &BoxTreeNode {
        &self.nodes[id.0]
    }

    /// Get a mutable reference to a node.
    pub fn node_mut(&mut self, id: NodeId) -> &mut BoxTreeNode {
        &mut self.nodes[id.0]
    }

    /// Get the root node ID.
    pub fn root(&self) -> NodeId {
        self.root
    }

    /// Get the children of a node.
    pub fn children(&self, id: NodeId) -> &[NodeId] {
        &self.nodes[id.0].children
    }

    /// Get the style of a node.
    pub fn style(&self, id: NodeId) -> &ComputedStyle {
        &self.nodes[id.0].style
    }

    /// Get the parent of a node.
    pub fn parent(&self, id: NodeId) -> Option<NodeId> {
        self.nodes[id.0].parent
    }

    /// Total number of nodes.
    pub fn len(&self) -> usize {
        self.nodes.len()
    }

    pub fn is_empty(&self) -> bool {
        self.nodes.is_empty()
    }

    /// Determine the formatting context for a node's children.
    pub fn formatting_context(&self, id: NodeId) -> FormattingContextType {
        let style = self.style(id);
        match style.display.inner {
            DisplayInner::Flex => FormattingContextType::Flex,
            DisplayInner::Grid => FormattingContextType::Grid,
            DisplayInner::Table => FormattingContextType::Table,
            DisplayInner::Flow | DisplayInner::FlowRoot => {
                // Determine if children are all block or all inline
                let children = self.children(id);
                if children.is_empty() {
                    return FormattingContextType::Block;
                }

                let has_block = children.iter().any(|&c| {
                    let cs = self.style(c);
                    cs.display.is_block_level() && !cs.is_out_of_flow()
                });

                if has_block {
                    FormattingContextType::Block
                } else {
                    FormattingContextType::Inline
                }
            }
            _ => FormattingContextType::Block,
        }
    }
}

impl Default for BoxTree {
    fn default() -> Self {
        Self::new()
    }
}

/// The type of formatting context established by a container.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FormattingContextType {
    Block,
    Inline,
    Flex,
    Grid,
    Table,
}

/// Helper to build a box tree from a simple description.
pub struct BoxTreeBuilder {
    pub tree: BoxTree,
}

impl BoxTreeBuilder {
    pub fn new() -> Self {
        Self {
            tree: BoxTree::new(),
        }
    }

    /// Add the root element.
    pub fn root(&mut self, style: ComputedStyle) -> NodeId {
        let id = self.tree.add_node(NodeKind::Element, style);
        self.tree.set_root(id);
        id
    }

    /// Add an element child.
    pub fn element(&mut self, parent: NodeId, style: ComputedStyle) -> NodeId {
        let id = self.tree.add_node(NodeKind::Element, style);
        self.tree.append_child(parent, id);
        id
    }

    /// Add a text child.
    pub fn text(&mut self, parent: NodeId, text: &str) -> NodeId {
        let id = self.tree.add_node(
            NodeKind::Text(TextContent {
                text: text.to_string(),
            }),
            ComputedStyle::inline(),
        );
        self.tree.append_child(parent, id);
        id
    }

    /// Finish building and return the tree.
    pub fn build(self) -> BoxTree {
        self.tree
    }
}

impl Default for BoxTreeBuilder {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::style::ComputedStyle;

    #[test]
    fn test_build_simple_tree() {
        let mut builder = BoxTreeBuilder::new();
        let root = builder.root(ComputedStyle::block());
        let child1 = builder.element(root, ComputedStyle::block());
        let child2 = builder.element(root, ComputedStyle::block());
        builder.text(child1, "Hello");
        let tree = builder.build();

        assert_eq!(tree.len(), 4);
        assert_eq!(tree.children(root).len(), 2);
        assert_eq!(tree.children(child1).len(), 1);
        assert_eq!(tree.children(child2).len(), 0);
        assert_eq!(tree.parent(child1), Some(root));
    }

    #[test]
    fn test_formatting_context_detection() {
        let mut builder = BoxTreeBuilder::new();
        let root = builder.root(ComputedStyle::block());
        builder.element(root, ComputedStyle::block());
        builder.element(root, ComputedStyle::block());
        let tree = builder.build();

        assert_eq!(tree.formatting_context(root), FormattingContextType::Block);
    }
}