use crate::glyphs::OP_BAND;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Block {
pub lines: Vec<Vec<char>>,
pub baseline: usize,
pub caret: Option<(usize, usize)>,
pub marks: Vec<(usize, usize, char)>,
}
impl Block {
pub fn new(lines: Vec<Vec<char>>, baseline: usize) -> Self {
Block {
lines,
baseline,
caret: None,
marks: vec![],
}
}
pub(super) fn with_caret(mut self, r: usize, c: usize) -> Self {
self.caret = Some((r, c));
self
}
pub(super) fn caret_marker() -> Self {
Block::empty().with_caret(0, 0)
}
pub fn empty() -> Self {
Block::new(vec![], 0)
}
pub fn from_chars(chars: Vec<char>) -> Self {
Block::new(vec![chars], 0)
}
pub fn width(&self) -> usize {
self.lines.first().map_or(0, |l| l.len())
}
pub fn height(&self) -> usize {
self.lines.len()
}
pub fn is_empty(&self) -> bool {
self.lines.is_empty()
}
pub fn to_strings(&self) -> Vec<String> {
self.lines.iter().map(|l| l.iter().collect()).collect()
}
pub fn to_text(&self) -> String {
self.to_strings()
.iter()
.map(|l| l.trim_end())
.collect::<Vec<_>>()
.join("\n")
}
pub(super) fn baseline_edge(&self, first: bool) -> Option<char> {
let line = self.lines.get(self.baseline)?;
if first { line.first() } else { line.last() }.copied()
}
}
pub(super) fn hcat(blocks: &[Block]) -> Block {
let blocks: Vec<&Block> = blocks
.iter()
.filter(|b| !b.is_empty() || b.caret.is_some() || !b.marks.is_empty())
.collect();
if blocks.is_empty() {
return Block::empty();
}
if blocks.iter().all(|b| b.is_empty()) {
let mut out = Block::empty();
for b in &blocks {
if b.caret.is_some() {
out.caret = Some((0, 0));
}
out.marks
.extend(b.marks.iter().map(|&(_, _, ch)| (0, 0, ch)));
}
return out;
}
let above = blocks.iter().map(|b| b.baseline).max().unwrap();
let below = blocks
.iter()
.map(|b| b.height().saturating_sub(b.baseline))
.max()
.unwrap();
let height = above + below;
let width: usize = blocks.iter().map(|b| b.width()).sum();
let mut grid = vec![vec![' '; width]; height];
let mut caret = None;
let mut marks = Vec::new();
let mut x = 0;
for b in blocks {
let y0 = above - b.baseline;
for (dy, line) in b.lines.iter().enumerate() {
for (dx, &c) in line.iter().enumerate() {
grid[y0 + dy][x + dx] = c;
}
}
if let Some((r, c)) = b.caret {
caret = Some(if b.is_empty() {
(above, x)
} else {
(y0 + r, x + c)
});
}
marks.extend(b.marks.iter().map(|&(r, c, ch)| {
if b.is_empty() {
(above, x, ch)
} else {
(y0 + r, x + c, ch)
}
}));
x += b.width();
}
Block {
lines: grid,
baseline: above,
caret,
marks,
}
}
#[derive(Default)]
pub(super) struct Annots {
caret: Option<(usize, usize)>,
marks: Vec<(usize, usize, char)>,
}
impl Annots {
pub(super) fn centered(mut self, b: &Block, width: usize, row_off: usize) -> Self {
let left = (width - b.width()) / 2;
self.caret = self
.caret
.or_else(|| b.caret.map(|(r, c)| (r + row_off, c + left)));
self.marks.extend(
b.marks
.iter()
.map(|&(r, c, ch)| (r + row_off, c + left, ch)),
);
self
}
pub(super) fn into_block(self, lines: Vec<Vec<char>>, baseline: usize) -> Block {
Block {
lines,
baseline,
caret: self.caret,
marks: self.marks,
}
}
}
pub(super) fn center_pad(b: &Block, width: usize) -> Vec<Vec<char>> {
let left = (width - b.width()) / 2;
b.lines
.iter()
.map(|line| {
let mut row = vec![' '; width];
row[left..left + line.len()].copy_from_slice(line);
row
})
.collect()
}
pub(super) fn vstack(blocks: &[Block]) -> Block {
let width = blocks.iter().map(|b| b.width()).max().unwrap_or(0).max(1);
let mut lines: Vec<Vec<char>> = Vec::new();
let mut caret = None;
let mut marks = Vec::new();
let mut baseline = 0;
for (i, b) in blocks.iter().enumerate() {
if i > 0 {
let mut sep = vec![' '; width];
sep[0] = OP_BAND;
lines.push(sep);
}
let y0 = lines.len();
if i == 0 {
baseline = b.baseline.min(b.height().saturating_sub(1));
}
if b.is_empty() {
lines.push(vec![' '; width]);
}
for line in &b.lines {
let mut l = line.clone();
l.resize(width, ' ');
lines.push(l);
}
if let Some((r, c)) = b.caret {
caret = Some((y0 + r.min(b.height().saturating_sub(1)), c));
}
marks.extend(b.marks.iter().map(|&(r, c, ch)| (y0 + r, c, ch)));
}
Block {
lines,
baseline,
caret,
marks,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn b(lines: &[&str], baseline: usize) -> Block {
Block::new(
lines.iter().map(|l| l.chars().collect()).collect(),
baseline,
)
}
#[test]
fn hcat_carries_all_channels() {
let mut left = b(&["ab"], 0);
left.marks.push((0, 0, '\u{E0F0}'));
let mut right = b(&["c", "d"], 1);
right.caret = Some((1, 0));
let out = hcat(&[left, right]);
assert_eq!(out.baseline, 1);
assert_eq!(out.to_strings(), vec![" c", "abd"]);
assert_eq!(out.marks, vec![(1, 0, '\u{E0F0}')]);
assert_eq!(out.caret, Some((1, 2)), "caret shifted by left's width");
}
#[test]
fn vstack_carries_all_channels() {
let mut top = b(&["x"], 0);
top.caret = Some((0, 0));
let mut bot = b(&["yy"], 0);
bot.marks.push((0, 0, '\u{E0F1}'));
let out = vstack(&[top, bot]);
assert_eq!(out.caret, Some((0, 0)), "caret stays in its own row");
let sep = 1;
assert_eq!(out.marks, vec![(sep + 1, 0, '\u{E0F1}')]);
}
#[test]
fn baseline_at_height_survives_composition() {
let sup = Block::new(vec!["2".chars().collect()], 1);
assert_eq!(sup.baseline, sup.height());
let out = hcat(&[b(&["x"], 0), sup]);
assert_eq!(out.to_strings(), vec![" 2", "x "]);
}
}