use crate::grid::Grid;
use crate::primitives::{Box, BoxStyle};
const fn is_top_left_corner(ch: char) -> bool {
matches!(ch, '┌' | '╔' | '╭')
}
const fn is_top_right_corner(ch: char) -> bool {
matches!(ch, '┐' | '╗' | '╮')
}
const fn is_bottom_left_corner(ch: char) -> bool {
matches!(ch, '└' | '╚' | '╰')
}
const fn is_bottom_right_corner(ch: char) -> bool {
matches!(ch, '┘' | '╝' | '╯')
}
const fn is_horizontal_border(ch: char) -> bool {
matches!(
ch,
'─' | '┬'
| '┴'
| '┼'
| '├'
| '┤'
| '═'
| '╦'
| '╩'
| '╬'
| '▼'
| '▲'
| '↓'
| '↑'
| '⇓'
| '⇑'
)
}
const fn is_vertical_border(ch: char) -> bool {
matches!(
ch,
'│' | '├'
| '┤'
| '┼'
| '┬'
| '┴'
| '║'
| '╠'
| '╣'
| '╬'
| '►'
| '◄'
| '→'
| '←'
| '⇒'
| '⇐'
)
}
pub struct BoxDetector<'a> {
grid: &'a Grid,
}
impl<'a> BoxDetector<'a> {
#[must_use]
pub const fn new(grid: &'a Grid) -> Self {
BoxDetector { grid }
}
#[must_use]
pub fn detect(self) -> Vec<Box> {
let mut boxes = Vec::new();
for row in 0..self.grid.height() {
for col in 0..self.grid.width() {
if let Some(ch) = self.grid.get(row, col) {
if is_top_left_corner(ch) {
if let Some(b) = self.trace_box(row, col, ch) {
boxes.push(b);
}
}
}
}
}
boxes
}
fn trace_box(&self, top_row: usize, left_col: usize, tl_char: char) -> Option<Box> {
let style = BoxStyle::from_corner(tl_char)?;
let bottom_row = self.trace_vertical(left_col, top_row + 1, is_bottom_left_corner)?;
let top_right_col = self.trace_horizontal(top_row, left_col + 1, is_top_right_corner)?;
let bottom_right_col =
self.trace_horizontal(bottom_row, left_col + 1, is_bottom_right_corner)?;
let right_col = top_right_col.max(bottom_right_col);
let actual_right_col =
self.find_right_border_extent(right_col, top_right_col, top_row + 1, bottom_row)?;
Some(Box {
top_left: (top_row, left_col),
bottom_right: (bottom_row, actual_right_col.max(right_col)),
style,
parent_idx: None,
child_indices: Vec::new(),
})
}
fn trace_horizontal(
&self,
row: usize,
start_col: usize,
is_target_corner: fn(char) -> bool,
) -> Option<usize> {
let mut col = start_col;
loop {
let ch = self.grid.get(row, col)?;
if is_target_corner(ch) {
return Some(col);
}
if !is_horizontal_border(ch) {
return None;
}
col += 1;
}
}
fn trace_vertical(
&self,
col: usize,
start_row: usize,
is_target_corner: fn(char) -> bool,
) -> Option<usize> {
let mut row = start_row;
loop {
let ch = self.grid.get(row, col)?;
if is_target_corner(ch) {
return Some(row);
}
if !is_vertical_border(ch) {
return None;
}
row += 1;
}
}
fn find_right_border_extent(
&self,
primary_col: usize,
alt_col: usize,
start_row: usize,
end_row: usize,
) -> Option<usize> {
let candidates: [Option<usize>; 4] = [
Some(primary_col),
if alt_col == primary_col {
None
} else {
Some(alt_col)
},
primary_col.checked_sub(1),
Some(primary_col + 1),
];
let mut max_col = primary_col;
for row in start_row..end_row {
let mut found = false;
for col_opt in &candidates {
if let Some(col) = *col_opt {
if matches!(self.grid.get(row, col), Some(ch) if is_vertical_border(ch)) {
max_col = max_col.max(col);
found = true;
break;
}
}
}
if !found {
return None;
}
}
Some(max_col)
}
}
#[allow(dead_code)] #[must_use]
pub fn detect_boxes(grid: &Grid) -> Vec<Box> {
BoxDetector::new(grid).detect()
}