Skip to main content

tix/ui/board/
hit.rs

1#[allow(clippy::wildcard_imports)]
2use super::*;
3
4impl BoardState {
5    /// Map a terminal pointer `(x, y)` — given against the same `area` passed
6    /// to [`render`] — to the card or column underneath. `status` mirrors
7    /// whether [`render`] is drawing a status message, so the status row (not
8    /// part of any column) never resolves to a target. Returns `None` for
9    /// positions outside the board area (`y` above the columns or beyond the
10    /// board) or when nothing is drawn. Uses [`compute_column_rects`] and
11    /// [`super::widgets::card_line_count`] — the geometry `render` draws with —
12    /// so the hit-tested layout matches the rendered one by construction (D3).
13    pub fn hit_test(
14        &self,
15        area: Rect,
16        display: BoardDisplay,
17        status: bool,
18        x: u16,
19        y: u16,
20    ) -> Option<HitTarget> {
21        if area.height < 2 || area.width < 4 || self.total_cols() == 0 {
22            return None;
23        }
24        let board_height = area.height.saturating_sub(u16::from(status));
25        if y >= area.y + board_height {
26            return None;
27        }
28        let board_area = Rect {
29            height: board_height,
30            ..area
31        };
32
33        let rects = compute_column_rects(board_area, self.total_cols());
34        let (col, col_rect) = rects
35            .iter()
36            .enumerate()
37            .find(|(_, r)| r.contains(Position::new(x, y)))?;
38
39        let cards: &[Card] = if col < self.columns.len() {
40            &self.columns[col].cards
41        } else {
42            &self.mismatch.cards
43        };
44
45        let group_mode = self
46            .filter
47            .as_deref()
48            .and_then(|q| crate::search::control_token(q, "group"));
49
50        // Items stack top-down exactly as `render` pushes them: group headers
51        // occupy one row each, cards occupy `card_line_count` rows.
52        let mut items: Vec<Option<usize>> = Vec::new();
53        if let Some(mode) = group_mode.as_deref() {
54            if let Some(sections) = Self::group_sections(cards, Some(mode)) {
55                for section in sections {
56                    items.push(None);
57                    for (j, card) in cards
58                        .iter()
59                        .enumerate()
60                        .take(section.end)
61                        .skip(section.start)
62                    {
63                        let count = widgets::card_line_count(
64                            card,
65                            col_rect.width as usize,
66                            display.wrap_titles,
67                            display.title_lines,
68                        );
69                        for _ in 0..count {
70                            items.push(Some(j));
71                        }
72                    }
73                }
74            }
75        } else {
76            for (j, card) in cards.iter().enumerate() {
77                let count = widgets::card_line_count(
78                    card,
79                    col_rect.width as usize,
80                    display.wrap_titles,
81                    display.title_lines,
82                );
83                for _ in 0..count {
84                    items.push(Some(j));
85                }
86            }
87        }
88
89        let has_footer = BoardState::group_footer(cards, group_mode.as_deref()).is_some();
90        let list_height = col_rect.height.saturating_sub(u16::from(has_footer));
91        // Content rows live between the block's top border and bottom border.
92        let content_top = col_rect.y + 1;
93        let content_end = col_rect.y + list_height.saturating_sub(1);
94        if y >= content_top && y < content_end {
95            let offset = usize::from(y - content_top);
96            if let Some(Some(row)) = items.get(offset) {
97                return Some(HitTarget::Card { col, row: *row });
98            }
99        }
100        Some(HitTarget::Column { col })
101    }
102}