termtui 0.1.0

A framework for building beautiful, responsive terminal user interfaces with a DOM-style hierarchical approach
Documentation
//! Virtual DOM diffing algorithm for efficient UI updates.
//!
//! This module implements a diffing algorithm that compares the current render tree
//! with a new node tree to generate a minimal set of patches. These patches
//! describe the changes needed to update the UI efficiently.
//!
//! ## Algorithm Overview
//!
//! ```text
//!     Old Tree              New Tree
//!     ┌───────┐            ┌───────┐
//!     │element│            │element│
//!     └───┬───┘            └───┬───┘
//!         │                    │
//!     ┌───┴───┐            ┌───┴───┐
//!     │ "old" │            │ "new" │
//!     └───────┘            └───────┘
//!         │                    │
//!         └───────diff─────────┘
//!//!//!             [ UpdateText ]
//! ```
//!
//! The diff algorithm recursively compares nodes and generates patches for:
//! - Text content changes
//! - Property/style updates
//! - Child additions/removals
//! - Node replacements

use crate::render_tree::{RenderNode, RenderNodeType};
use crate::vnode::VNode;
use std::cell::RefCell;
use std::rc::Rc;

//--------------------------------------------------------------------------------------------------
// Types
//--------------------------------------------------------------------------------------------------

/// Represents a single change operation to apply to the render tree.
///
/// Patches are generated by the diff algorithm and applied by the VDom
/// to update the UI efficiently without full re-renders.
#[derive(Debug, Clone)]
pub enum Patch {
    /// Replace an entire node with a new node.
    /// Used when nodes are fundamentally different (e.g., container -> text).
    Replace {
        old: Rc<RefCell<RenderNode>>,
        new: VNode,
    },

    /// Update the text content and style of a text node.
    /// More efficient than replacing the entire node.
    UpdateText {
        node: Rc<RefCell<RenderNode>>,
        new_text: String,
        new_style: Option<crate::style::TextStyle>,
    },

    /// Update the spans of a styled text node.
    UpdateRichText {
        node: Rc<RefCell<RenderNode>>,
        new_spans: Vec<crate::node::TextSpan>,
    },

    /// Update the properties (style, dimensions) of a div.
    /// Preserves the node structure while updating visual properties.
    UpdateProps {
        node: Rc<RefCell<RenderNode>>,
        div: crate::node::Div<VNode>,
    },

    /// Add a new child node at a specific position.
    AddChild {
        parent: Rc<RefCell<RenderNode>>,
        child: VNode,
        index: usize,
    },

    /// Remove a child node at a specific position.
    RemoveChild {
        parent: Rc<RefCell<RenderNode>>,
        index: usize,
    },

    /// Reorder children by applying a series of moves.
    /// Currently unused but reserved for future keyed diff implementation.
    ReorderChildren {
        parent: Rc<RefCell<RenderNode>>,
        moves: Vec<Move>,
    },
}

/// Represents a move operation for reordering children.
/// Specifies moving a child from one index to another.
#[derive(Debug, Clone)]
pub struct Move {
    /// Source index of the child to move
    pub from: usize,

    /// Target index where the child should be moved to
    pub to: usize,
}

/// Context for accumulating patches during the diff process.
/// Passed through the recursive diff algorithm to collect all changes.
pub struct DiffContext {
    /// Accumulated patches discovered during diffing
    pub patches: Vec<Patch>,
}

//--------------------------------------------------------------------------------------------------
// Functions
//--------------------------------------------------------------------------------------------------

/// Performs a diff between an existing render tree and a new virtual node tree.
///
/// Returns a vector of patches that describe the minimal changes needed
/// to transform the old tree into the new tree.
///
/// ## Example
///
/// ```text
///     diff(old_tree, new_tree) → [
///         UpdateText { node: text_node, new_text: "updated", new_style: Some(...) },
///         AddChild { parent: div_node, child: new_span, index: 2 }
///     ]
/// ```
pub fn diff(old: &Rc<RefCell<RenderNode>>, new: &VNode) -> Vec<Patch> {
    let mut context = DiffContext {
        patches: Vec::new(),
    };

    diff_node(&mut context, old, new);
    context.patches
}

/// Recursively diffs a single node and its subtree.
///
/// Compares node types and delegates to specialized diff functions
/// based on the node type combination.
fn diff_node(context: &mut DiffContext, old: &Rc<RefCell<RenderNode>>, new: &VNode) {
    let old_ref = old.borrow();

    match (&old_ref.node_type, new) {
        (RenderNodeType::Text(old_text), VNode::Text(new_text)) => {
            // Check if either content or style has changed
            let old_style = old_ref.text_style.as_ref();
            let new_style = new_text.style.as_ref();
            let style_changed = match (old_style, new_style) {
                (None, None) => false,
                (Some(old), Some(new)) => old != new,
                _ => true,
            };

            if old_text != &new_text.content || style_changed {
                context.patches.push(Patch::UpdateText {
                    node: old.clone(),
                    new_text: new_text.content.clone(),
                    new_style: new_text.style.clone(),
                });
            }
        }
        (RenderNodeType::RichText(old_spans), VNode::RichText(new_rich)) => {
            // Check if spans have changed
            if old_spans != &new_rich.spans {
                context.patches.push(Patch::UpdateRichText {
                    node: old.clone(),
                    new_spans: new_rich.spans.clone(),
                });
            }
        }
        (RenderNodeType::Element, VNode::Div(new_div)) => {
            diff_div(context, old, &old_ref, new_div);
        }
        _ => {
            context.patches.push(Patch::Replace {
                old: old.clone(),
                new: new.clone(),
            });
        }
    }
}

/// Diffs two div nodes, checking for property changes and recursing on children.
///
/// Generates UpdateProps patches for style/dimension changes and delegates
/// to diff_children for child node comparison.
fn diff_div(
    context: &mut DiffContext,
    old_node: &Rc<RefCell<RenderNode>>,
    old_ref: &RenderNode,
    new_div: &crate::node::Div<VNode>,
) {
    let props_changed = {
        let old_style = &old_ref.style;
        // Use the OLD node's focus state, not the new div's (which is always false)
        let is_focused = old_ref.focused;
        // Get effective style based on the preserved focus state
        let new_style = if is_focused {
            crate::style::Style::merge(new_div.styles.base.clone(), new_div.styles.focus.clone())
        } else {
            new_div.styles.base.clone()
        };
        let new_style = &new_style;

        // Check if dimensions changed (including percentage values)
        let dimensions_changed = match (old_style, new_style) {
            (Some(old_s), Some(new_s)) => {
                old_s.width != new_s.width || old_s.height != new_s.height
            }
            (None, Some(_)) | (Some(_), None) => true,
            (None, None) => false,
        };

        old_style != new_style || dimensions_changed
    };

    if props_changed {
        context.patches.push(Patch::UpdateProps {
            node: old_node.clone(),
            div: new_div.clone(),
        });
    }

    diff_children(context, old_node, &old_ref.children, &new_div.children);
}

/// Diffs two lists of children, handling additions, removals, and updates.
///
/// Currently implements a simple index-based diff:
/// 1. Diffs common children by index
/// 2. Adds new children if new list is longer
/// 3. Removes extra children if old list is longer
///
/// ## Future Enhancement
///
/// Could be optimized with a key-based algorithm for better handling
/// of reordered children (like React's reconciliation).
fn diff_children(
    context: &mut DiffContext,
    parent: &Rc<RefCell<RenderNode>>,
    old_children: &[Rc<RefCell<RenderNode>>],
    new_children: &[VNode],
) {
    let old_len = old_children.len();
    let new_len = new_children.len();
    let min_len = old_len.min(new_len);

    for i in 0..min_len {
        diff_node(context, &old_children[i], &new_children[i]);
    }

    if new_len > old_len {
        for (i, child) in new_children.iter().enumerate().skip(old_len) {
            context.patches.push(Patch::AddChild {
                parent: parent.clone(),
                child: child.clone(),
                index: i,
            });
        }
    } else if old_len > new_len {
        for i in (new_len..old_len).rev() {
            context.patches.push(Patch::RemoveChild {
                parent: parent.clone(),
                index: i,
            });
        }
    }
}