tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

impl BoardState {
    /// Map a terminal pointer `(x, y)` — given against the same `area` passed
    /// to [`render`] — to the card or column underneath. `status` mirrors
    /// whether [`render`] is drawing a status message, so the status row (not
    /// part of any column) never resolves to a target. Returns `None` for
    /// positions outside the board area (`y` above the columns or beyond the
    /// board) or when nothing is drawn. Uses [`compute_column_rects`] and
    /// [`super::widgets::card_line_count`] — the geometry `render` draws with —
    /// so the hit-tested layout matches the rendered one by construction (D3).
    pub fn hit_test(
        &self,
        area: Rect,
        display: BoardDisplay,
        status: bool,
        x: u16,
        y: u16,
    ) -> Option<HitTarget> {
        if area.height < 2 || area.width < 4 || self.total_cols() == 0 {
            return None;
        }
        let board_height = area.height.saturating_sub(u16::from(status));
        if y >= area.y + board_height {
            return None;
        }
        let board_area = Rect {
            height: board_height,
            ..area
        };

        let rects = compute_column_rects(board_area, self.total_cols());
        let (col, col_rect) = rects
            .iter()
            .enumerate()
            .find(|(_, r)| r.contains(Position::new(x, y)))?;

        let cards: &[Card] = if col < self.columns.len() {
            &self.columns[col].cards
        } else {
            &self.mismatch.cards
        };

        let group_mode = self
            .filter
            .as_deref()
            .and_then(|q| crate::search::control_token(q, "group"));

        // Items stack top-down exactly as `render` pushes them: group headers
        // occupy one row each, cards occupy `card_line_count` rows.
        let mut items: Vec<Option<usize>> = Vec::new();
        if let Some(mode) = group_mode.as_deref() {
            if let Some(sections) = Self::group_sections(cards, Some(mode)) {
                for section in sections {
                    items.push(None);
                    for (j, card) in cards
                        .iter()
                        .enumerate()
                        .take(section.end)
                        .skip(section.start)
                    {
                        let count = widgets::card_line_count(
                            card,
                            col_rect.width as usize,
                            display.wrap_titles,
                            display.title_lines,
                        );
                        for _ in 0..count {
                            items.push(Some(j));
                        }
                    }
                }
            }
        } else {
            for (j, card) in cards.iter().enumerate() {
                let count = widgets::card_line_count(
                    card,
                    col_rect.width as usize,
                    display.wrap_titles,
                    display.title_lines,
                );
                for _ in 0..count {
                    items.push(Some(j));
                }
            }
        }

        let has_footer = BoardState::group_footer(cards, group_mode.as_deref()).is_some();
        let list_height = col_rect.height.saturating_sub(u16::from(has_footer));
        // Content rows live between the block's top border and bottom border.
        let content_top = col_rect.y + 1;
        let content_end = col_rect.y + list_height.saturating_sub(1);
        if y >= content_top && y < content_end {
            let offset = usize::from(y - content_top);
            if let Some(Some(row)) = items.get(offset) {
                return Some(HitTarget::Card { col, row: *row });
            }
        }
        Some(HitTarget::Column { col })
    }
}