crustkit 0.2.0

Small Ratatui and Crossterm primitives for terminal applications.
Documentation
use crossterm::event::{MouseButton, MouseEvent, MouseEventKind};
use ratatui::layout::Rect;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MouseClick {
    pub column: u16,
    pub row: u16,
}

pub fn left_mouse_click(event: MouseEvent) -> Option<MouseClick> {
    match event.kind {
        MouseEventKind::Down(MouseButton::Left) => Some(MouseClick {
            column: event.column,
            row: event.row,
        }),
        _ => None,
    }
}

pub fn mouse_position(event: MouseEvent) -> MouseClick {
    MouseClick {
        column: event.column,
        row: event.row,
    }
}

pub fn rect_contains(area: Rect, click: MouseClick) -> bool {
    click.column >= area.x
        && click.column < area.x.saturating_add(area.width)
        && click.row >= area.y
        && click.row < area.y.saturating_add(area.height)
}

pub fn row_index_at(
    area: Rect,
    click: MouseClick,
    first_row_offset: u16,
    row_count: usize,
) -> Option<usize> {
    if !rect_contains(area, click) {
        return None;
    }

    let first_row = area.y.saturating_add(first_row_offset);
    if click.row < first_row {
        return None;
    }

    let index = usize::from(click.row - first_row);
    let visible_rows = usize::from(
        area.height
            .saturating_sub(first_row_offset.saturating_add(1)),
    );
    (index < row_count && index < visible_rows).then_some(index)
}

pub fn inline_item_index_at(
    area: Rect,
    click: MouseClick,
    widths: impl IntoIterator<Item = u16>,
) -> Option<usize> {
    if click.row != area.y || click.column < area.x {
        return None;
    }

    let mut column = area.x;
    let max_column = area.x.saturating_add(area.width);
    for (index, width) in widths.into_iter().enumerate() {
        let next_column = column.saturating_add(width);
        if click.column >= column && click.column < next_column.min(max_column) {
            return Some(index);
        }
        column = next_column;
        if column >= max_column {
            return None;
        }
    }

    None
}

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

    #[test]
    fn row_index_accounts_for_widget_chrome() {
        let area = Rect::new(4, 10, 40, 8);

        assert_eq!(
            row_index_at(area, MouseClick { column: 6, row: 12 }, 2, 3),
            Some(0)
        );
        assert_eq!(
            row_index_at(area, MouseClick { column: 6, row: 15 }, 2, 3),
            None
        );
    }

    #[test]
    fn inline_item_index_uses_declared_widths() {
        let area = Rect::new(10, 2, 30, 2);
        let widths = [8, 10, 12];

        assert_eq!(
            inline_item_index_at(area, MouseClick { column: 20, row: 2 }, widths),
            Some(1)
        );
        assert_eq!(
            inline_item_index_at(area, MouseClick { column: 20, row: 3 }, widths),
            None
        );
    }
}