lv-tui 0.1.1

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
use crate::geom::{Rect, Size};
use crate::style::Length;

/// 布局约束
#[derive(Debug, Clone, Copy)]
pub struct Constraint {
    pub min: Size,
    pub max: Size,
}

impl Constraint {
    pub fn loose(max_width: u16, max_height: u16) -> Self {
        Self {
            min: Size::default(),
            max: Size {
                width: max_width,
                height: max_height,
            },
        }
    }
}

/// 布局项描述
pub struct LayoutItem {
    pub width: Length,
    pub height: Length,
}

/// 垂直布局:分配每个子项的 Rect
///
/// 分配顺序:Fixed → Percent → Fraction → Auto
/// MVP 简化:Fixed 优先,剩余空间 Fraction 按比例分配,Auto 占 1 行。
pub fn layout_vertical(rect: Rect, items: &[LayoutItem], gap: u16) -> Vec<Rect> {
    let mut rects = Vec::with_capacity(items.len());

    if items.is_empty() {
        return rects;
    }

    let total_gap = gap.saturating_mul(items.len().saturating_sub(1) as u16);
    let available_height = rect.height.saturating_sub(total_gap);

    // 第一遍:计算 Fixed 和 Auto 的高度,统计 Fraction 总权重
    let mut fixed_total: u16 = 0;
    let mut fraction_total: u16 = 0;
    let mut auto_count: u16 = 0;

    for item in items {
        match item.height {
            Length::Fixed(h) => fixed_total = fixed_total.saturating_add(h),
            Length::Fraction(w) => fraction_total = fraction_total.saturating_add(w),
            Length::Percent(_) | Length::Auto => auto_count = auto_count.saturating_add(1),
        }
    }

    // Fixed 项不超过可用高度
    let fixed_total = fixed_total.min(available_height);

    // Auto 每项 1 行
    let auto_total = auto_count;

    // 剩余给 Fraction 的空间
    let fraction_space = available_height
        .saturating_sub(fixed_total)
        .saturating_sub(auto_total);

    // 每个 fraction 单位的像素值
    let fraction_unit = if fraction_total > 0 {
        fraction_space / fraction_total
    } else {
        0
    };

    // 第二遍:分配 Rect
    let mut y_offset = rect.y;

    for item in items {
        let height = match item.height {
            Length::Fixed(h) => h.min(available_height),
            Length::Percent(p) => available_height.saturating_mul(p) / 100,
            Length::Fraction(w) => {
                // 最后一个 Fraction 项收尾,避免舍入误差
                let base = w.saturating_mul(fraction_unit);
                fraction_total = fraction_total.saturating_sub(w);
                if fraction_total == 0 {
                    // 把剩余空间全给它
                    rect.y.saturating_add(available_height).saturating_sub(y_offset)
                } else {
                    base
                }
            }
            Length::Auto => 1,
        };

        let height = if y_offset.saturating_add(height) > rect.y.saturating_add(available_height) {
            rect.y.saturating_add(available_height).saturating_sub(y_offset)
        } else {
            height
        };

        rects.push(Rect {
            x: rect.x,
            y: y_offset,
            width: rect.width,
            height,
        });

        y_offset = y_offset.saturating_add(height).saturating_add(gap);
    }

    rects
}

/// 水平布局:分配每个子项的 Rect
///
/// 分配顺序:Fixed → Percent → Fraction → Auto
pub fn layout_horizontal(rect: Rect, items: &[LayoutItem], gap: u16) -> Vec<Rect> {
    let mut rects = Vec::with_capacity(items.len());

    if items.is_empty() {
        return rects;
    }

    let total_gap = gap.saturating_mul(items.len().saturating_sub(1) as u16);
    let available_width = rect.width.saturating_sub(total_gap);

    // 第一遍:统计各类宽度
    let mut fixed_total: u16 = 0;
    let mut fraction_total: u16 = 0;
    let mut auto_count: u16 = 0;

    for item in items {
        match item.width {
            Length::Fixed(w) => fixed_total = fixed_total.saturating_add(w),
            Length::Fraction(w) => fraction_total = fraction_total.saturating_add(w),
            Length::Percent(_) | Length::Auto => auto_count = auto_count.saturating_add(1),
        }
    }

    let fixed_total = fixed_total.min(available_width);
    let auto_total = auto_count;
    let fraction_space = available_width
        .saturating_sub(fixed_total)
        .saturating_sub(auto_total);

    let fraction_unit = if fraction_total > 0 {
        fraction_space / fraction_total
    } else {
        0
    };

    // 第二遍:分配 Rect
    let mut x_offset = rect.x;

    for item in items {
        let width = match item.width {
            Length::Fixed(w) => w.min(available_width),
            Length::Percent(p) => available_width.saturating_mul(p) / 100,
            Length::Fraction(w) => {
                let base = w.saturating_mul(fraction_unit);
                fraction_total = fraction_total.saturating_sub(w);
                if fraction_total == 0 {
                    rect.x.saturating_add(available_width).saturating_sub(x_offset)
                } else {
                    base
                }
            }
            Length::Auto => 1,
        };

        let width = if x_offset.saturating_add(width) > rect.x.saturating_add(available_width) {
            rect.x.saturating_add(available_width).saturating_sub(x_offset)
        } else {
            width
        };

        rects.push(Rect {
            x: x_offset,
            y: rect.y,
            width,
            height: rect.height,
        });

        x_offset = x_offset.saturating_add(width).saturating_add(gap);
    }

    rects
}

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

    #[test]
    fn test_layout_vertical_fixed() {
        let rect = Rect { x: 0, y: 0, width: 10, height: 10 };
        let items = [
            LayoutItem { width: Length::Auto, height: Length::Fixed(3) },
            LayoutItem { width: Length::Auto, height: Length::Fixed(4) },
        ];
        let result = layout_vertical(rect, &items, 1);
        assert_eq!(result.len(), 2);
        assert_eq!(result[0], Rect { x: 0, y: 0, width: 10, height: 3 });
        assert_eq!(result[1], Rect { x: 0, y: 4, width: 10, height: 4 });
    }

    #[test]
    fn test_layout_horizontal_fixed() {
        let rect = Rect { x: 0, y: 0, width: 10, height: 5 };
        let items = [
            LayoutItem { width: Length::Fixed(3), height: Length::Auto },
            LayoutItem { width: Length::Fixed(4), height: Length::Auto },
        ];
        let result = layout_horizontal(rect, &items, 1);
        assert_eq!(result.len(), 2);
        assert_eq!(result[0], Rect { x: 0, y: 0, width: 3, height: 5 });
        assert_eq!(result[1], Rect { x: 4, y: 0, width: 4, height: 5 });
    }

    #[test]
    fn test_layout_horizontal_fraction() {
        let rect = Rect { x: 0, y: 0, width: 10, height: 5 };
        let items = [
            LayoutItem { width: Length::Fraction(1), height: Length::Auto },
            LayoutItem { width: Length::Fraction(1), height: Length::Auto },
        ];
        let result = layout_horizontal(rect, &items, 0);
        assert_eq!(result.len(), 2);
        assert_eq!(result[0].width, 5);
        assert_eq!(result[1].width, 5);
    }
}