ratatui-toolkit 0.2.6

DEPRECATED: this crate was renamed to `ratkit`. Please migrate to `ratkit`.
Documentation
use crate::primitives::resizable_grid::types::{
    LayoutNode, PaneId, ResizableGrid, SplitAxis, SplitDividerLayout, DEFAULT_SPLIT_PERCENT,
    MAX_SPLIT_PERCENT, MIN_SPLIT_PERCENT,
};
use ratatui::layout::Rect;

impl ResizableGrid {
    pub(super) fn split_pane(&mut self, pane_id: PaneId, axis: SplitAxis) -> Option<PaneId> {
        let pane_index = self.find_pane_node_index(pane_id)?;
        let new_pane_id = self.allocate_pane_id();
        let first_index = self.nodes.len();
        let second_index = self.nodes.len().saturating_add(1);

        self.nodes.push(LayoutNode::Pane { id: pane_id });
        self.nodes.push(LayoutNode::Pane { id: new_pane_id });
        self.nodes[pane_index] = LayoutNode::Split {
            axis,
            ratio: DEFAULT_SPLIT_PERCENT,
            first: first_index,
            second: second_index,
        };

        Some(new_pane_id)
    }

    pub fn split_pane_horizontally(&mut self, pane_id: PaneId) -> Option<PaneId> {
        self.split_pane(pane_id, SplitAxis::Horizontal)
    }

    pub fn split_pane_vertically(&mut self, pane_id: PaneId) -> Option<PaneId> {
        self.split_pane(pane_id, SplitAxis::Vertical)
    }

    pub fn resize_split(&mut self, split_index: usize, percent: u16) -> bool {
        let Some(LayoutNode::Split { ratio, .. }) = self.nodes.get_mut(split_index) else {
            return false;
        };

        *ratio = percent.clamp(MIN_SPLIT_PERCENT, MAX_SPLIT_PERCENT);
        true
    }

    pub fn resize_divider(&mut self, pane_id: PaneId, percent: u16) -> bool {
        let Some((parent_index, is_first)) = self.find_parent_split(pane_id) else {
            return false;
        };
        let Some(LayoutNode::Split { ratio, .. }) = self.nodes.get_mut(parent_index) else {
            return false;
        };

        let new_ratio = if is_first {
            percent
        } else {
            100_u16.saturating_sub(percent)
        };

        *ratio = new_ratio.clamp(MIN_SPLIT_PERCENT, MAX_SPLIT_PERCENT);
        true
    }

    pub(super) fn find_parent_split(&self, pane_id: PaneId) -> Option<(usize, bool)> {
        let mut stack = vec![(self.root_index, None)];

        while let Some((node_index, parent_index)) = stack.pop() {
            let Some(node) = self.nodes.get(node_index) else {
                continue;
            };

            match node {
                LayoutNode::Pane { id } => {
                    if *id == pane_id {
                        let parent_index = parent_index?;
                        let Some(LayoutNode::Split { first, .. }) = self.nodes.get(parent_index)
                        else {
                            return None;
                        };
                        let is_first = *first == node_index;
                        return Some((parent_index, is_first));
                    }
                }
                LayoutNode::Split { first, second, .. } => {
                    stack.push((*second, Some(node_index)));
                    stack.push((*first, Some(node_index)));
                }
            }
        }

        None
    }

    pub(super) fn find_pane_node_index(&self, pane_id: PaneId) -> Option<usize> {
        let mut stack = vec![self.root_index];

        while let Some(node_index) = stack.pop() {
            let Some(node) = self.nodes.get(node_index) else {
                continue;
            };

            match node {
                LayoutNode::Pane { id } => {
                    if *id == pane_id {
                        return Some(node_index);
                    }
                }
                LayoutNode::Split { first, second, .. } => {
                    stack.push(*second);
                    stack.push(*first);
                }
            }
        }

        None
    }

    pub fn move_pane(&mut self, pane_id: PaneId, target_pane_id: PaneId) -> bool {
        let Some(source_index) = self.find_pane_node_index(pane_id) else {
            return false;
        };
        let Some(target_index) = self.find_pane_node_index(target_pane_id) else {
            return false;
        };

        if source_index == target_index {
            return true;
        }

        let source_id = match self.nodes.get(source_index) {
            Some(LayoutNode::Pane { id }) => *id,
            _ => return false,
        };
        let target_id = match self.nodes.get(target_index) {
            Some(LayoutNode::Pane { id }) => *id,
            _ => return false,
        };

        self.nodes[source_index] = LayoutNode::Pane { id: target_id };
        self.nodes[target_index] = LayoutNode::Pane { id: source_id };

        true
    }

    pub fn remove_pane(&mut self, pane_id: PaneId) -> bool {
        let Some((parent_index, is_first)) = self.find_parent_split(pane_id) else {
            return false;
        };
        let Some(LayoutNode::Split { first, second, .. }) = self.nodes.get(parent_index) else {
            return false;
        };
        let sibling_index = if is_first { *second } else { *first };
        let Some(sibling_node) = self.nodes.get(sibling_index).cloned() else {
            return false;
        };

        self.nodes[parent_index] = sibling_node;
        true
    }

    pub(super) fn allocate_pane_id(&mut self) -> PaneId {
        let pane_id = self.next_pane_id;
        self.next_pane_id = self.next_pane_id.saturating_add(1);
        pane_id
    }

    pub fn get_split_ratio(&self, split_index: usize) -> Option<u16> {
        let Some(LayoutNode::Split { ratio, .. }) = self.nodes.get(split_index) else {
            return None;
        };
        Some(*ratio)
    }

    pub fn split_percent(&self) -> u16 {
        self.get_split_ratio(0).unwrap_or(DEFAULT_SPLIT_PERCENT)
    }

    pub fn min_percent(&self) -> u16 {
        MIN_SPLIT_PERCENT
    }

    pub fn max_percent(&self) -> u16 {
        MAX_SPLIT_PERCENT
    }

    pub fn set_split_percent(&mut self, percent: u16) {
        let _ = self.resize_split(0, percent);
    }

    pub fn is_hovering(&self) -> bool {
        self.hovered_split.is_some()
    }

    pub fn is_dragging(&self) -> bool {
        self.dragging_split.is_some()
    }

    pub fn start_drag(&mut self) {
        self.dragging_split = self.hovered_split;
    }

    pub fn stop_drag(&mut self) {
        self.dragging_split = None;
    }

    pub fn update_divider_position(&mut self, _area: Rect) {}

    pub fn is_on_divider(&self, mouse_column: u16, mouse_row: u16, area: Rect) -> bool {
        self.find_divider_at(mouse_column, mouse_row, area)
            .is_some()
    }

    pub fn find_divider_at(&self, column: u16, row: u16, area: Rect) -> Option<usize> {
        let layouts = self.layout_dividers_internal(area);
        let threshold = self.hit_threshold;

        for divider in &layouts {
            let rect = divider.area();
            match divider.axis() {
                SplitAxis::Vertical => {
                    let divider_x = rect.x.saturating_add(
                        ((rect.width as u32 * divider.ratio() as u32) / 100) as u16,
                    );
                    let distance = divider_x.abs_diff(column);
                    if distance <= threshold
                        && column <= divider_x.saturating_add(threshold)
                        && row >= rect.y
                        && row <= rect.y.saturating_add(rect.height)
                    {
                        return Some(divider.split_index());
                    }
                }
                SplitAxis::Horizontal => {
                    let divider_y = rect.y.saturating_add(
                        ((rect.height as u32 * divider.ratio() as u32) / 100) as u16,
                    );
                    let distance = divider_y.abs_diff(row);
                    if distance <= threshold
                        && row <= divider_y.saturating_add(threshold)
                        && column >= rect.x
                        && column <= rect.x.saturating_add(rect.width)
                    {
                        return Some(divider.split_index());
                    }
                }
            }
        }
        None
    }

    pub fn update_from_mouse(&mut self, mouse_column: u16, mouse_row: u16, area: Rect) {
        if self.dragging_split.is_none() {
            return;
        }

        let Some(split_index) = self.dragging_split else {
            return;
        };

        let layouts = self.layout_dividers_internal(area);
        let divider_layout = layouts
            .iter()
            .find(|divider| divider.split_index() == split_index);

        if let Some(divider) = divider_layout {
            let rect = divider.area();
            match divider.axis() {
                SplitAxis::Vertical => {
                    let content_width = rect.width;
                    if content_width > 0 {
                        let relative_x = mouse_column.saturating_sub(rect.x);
                        let percent = ((relative_x as u32 * 100) / content_width as u32) as u16;
                        let _ = self.resize_split(split_index, percent);
                    }
                }
                SplitAxis::Horizontal => {
                    let content_height = rect.height;
                    if content_height > 0 {
                        let relative_y = mouse_row.saturating_sub(rect.y);
                        let percent = ((relative_y as u32 * 100) / content_height as u32) as u16;
                        let _ = self.resize_split(split_index, percent);
                    }
                }
            }
        }
    }

    fn layout_dividers_internal(&self, area: Rect) -> Vec<SplitDividerLayout> {
        let mut dividers = Vec::new();
        let mut stack = vec![(self.root_index, area)];

        while let Some((node_index, node_area)) = stack.pop() {
            let Some(node) = self.nodes.get(node_index) else {
                continue;
            };

            if let LayoutNode::Split {
                axis,
                ratio,
                first,
                second,
            } = node
            {
                dividers.push(SplitDividerLayout {
                    split_index: node_index,
                    axis: *axis,
                    area: node_area,
                    ratio: *ratio,
                });

                match axis {
                    SplitAxis::Vertical => {
                        let first_width = ((node_area.width as u32 * *ratio as u32) / 100) as u16;
                        let second_width = node_area.width.saturating_sub(first_width);
                        let first_area = Rect {
                            x: node_area.x,
                            y: node_area.y,
                            width: first_width,
                            height: node_area.height,
                        };
                        let second_area = Rect {
                            x: node_area.x.saturating_add(first_width),
                            y: node_area.y,
                            width: second_width,
                            height: node_area.height,
                        };
                        stack.push((*second, second_area));
                        stack.push((*first, first_area));
                    }
                    SplitAxis::Horizontal => {
                        let first_height = ((node_area.height as u32 * *ratio as u32) / 100) as u16;
                        let second_height = node_area.height.saturating_sub(first_height);
                        let first_area = Rect {
                            x: node_area.x,
                            y: node_area.y,
                            width: node_area.width,
                            height: first_height,
                        };
                        let second_area = Rect {
                            x: node_area.x,
                            y: node_area.y.saturating_add(first_height),
                            width: node_area.width,
                            height: second_height,
                        };
                        stack.push((*second, second_area));
                        stack.push((*first, first_area));
                    }
                }
            }
        }

        dividers
    }
}