use crate::cell::{Cell, CellFlags};
use crate::color::Color;
use core::num::NonZeroU32;
use std::collections::BTreeMap;
use std::ops::{Deref, DerefMut};
type Combining = BTreeMap<usize, Vec<char>>;
type Links = BTreeMap<usize, NonZeroU32>;
type UColors = BTreeMap<usize, Color>;
fn move_map<V>(map: &mut BTreeMap<usize, V>, src: std::ops::Range<usize>, dst: usize) {
if map.is_empty() {
return;
}
let start = src.start;
let moved: Vec<(usize, V)> = src
.filter_map(|s| map.remove(&s).map(|v| (dst + (s - start), v)))
.collect();
for (col, v) in moved {
map.insert(col, v);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub struct Row {
cells: Vec<Cell>,
combining: Combining,
links: Links,
ucolors: UColors,
}
impl Row {
pub(crate) fn blank(cols: usize) -> Row {
Row {
cells: vec![Cell::default(); cols],
combining: Combining::new(),
links: Links::new(),
ucolors: UColors::new(),
}
}
pub(crate) fn from_cells(cells: Vec<Cell>) -> Row {
Row {
cells,
combining: Combining::new(),
links: Links::new(),
ucolors: UColors::new(),
}
}
pub(crate) fn new(
cells: Vec<Cell>,
combining: Combining,
links: Links,
ucolors: UColors,
) -> Row {
Row {
cells,
combining,
links,
ucolors,
}
}
pub(crate) fn into_parts(self) -> (Vec<Cell>, Combining, Links, UColors) {
(self.cells, self.combining, self.links, self.ucolors)
}
pub(crate) fn resize(&mut self, cols: usize) {
self.cells.resize(cols, Cell::default());
if self
.combining
.keys()
.next_back()
.is_some_and(|&m| m >= cols)
{
self.combining.retain(|&col, _| col < cols);
}
if self.links.keys().next_back().is_some_and(|&m| m >= cols) {
self.links.retain(|&col, _| col < cols);
}
if self.ucolors.keys().next_back().is_some_and(|&m| m >= cols) {
self.ucolors.retain(|&col, _| col < cols);
}
}
pub(crate) fn clear(&mut self) {
self.cells.clear();
self.combining.clear();
self.links.clear();
self.ucolors.clear();
}
pub(crate) fn combining_at(&self, col: usize) -> Option<&[char]> {
if self.cells[col].is_combined() {
self.combining.get(&col).map(Vec::as_slice)
} else {
None
}
}
pub(crate) fn link_at(&self, col: usize) -> Option<NonZeroU32> {
if self.cells[col].is_linked() {
self.links.get(&col).copied()
} else {
None
}
}
pub(crate) fn ucolor_at(&self, col: usize) -> Option<Color> {
if self.cells[col].is_ucolored() {
self.ucolors.get(&col).copied()
} else {
None
}
}
pub(crate) fn push_combining(&mut self, col: usize, mark: char) {
if self.cells[col].is_combined() {
self.combining.entry(col).or_default().push(mark);
} else {
self.cells[col].set_combined(true);
self.combining.insert(col, vec![mark]);
}
}
pub(crate) fn set_link(&mut self, col: usize, link: NonZeroU32) {
self.cells[col].set_linked(true);
self.links.insert(col, link);
}
pub(crate) fn set_ucolor(&mut self, col: usize, color: Color) {
self.cells[col].set_ucolored(true);
self.ucolors.insert(col, color);
}
pub(crate) fn move_maps(&mut self, src: std::ops::Range<usize>, dst: usize) {
move_map(&mut self.combining, src.clone(), dst);
move_map(&mut self.links, src.clone(), dst);
move_map(&mut self.ucolors, src, dst);
}
}
impl Deref for Row {
type Target = [Cell];
fn deref(&self) -> &[Cell] {
&self.cells
}
}
impl DerefMut for Row {
fn deref_mut(&mut self) -> &mut [Cell] {
&mut self.cells
}
}
pub(crate) fn reflow(
rows: Vec<Row>,
new_cols: usize,
points: &[(usize, usize)],
) -> (Vec<Row>, Vec<(usize, usize)>) {
let mut logical: Vec<Vec<Cell>> = Vec::new();
let mut logical_comb: Vec<Combining> = Vec::new();
let mut logical_links: Vec<Links> = Vec::new();
let mut logical_ucolors: Vec<UColors> = Vec::new();
let mut current: Vec<Cell> = Vec::new();
let mut current_comb: Combining = Combining::new();
let mut current_links: Links = Links::new();
let mut current_ucolors: UColors = UColors::new();
let mut tracked: Vec<(usize, usize, bool)> = vec![(0, 0, false); points.len()];
for (i, row) in rows.into_iter().enumerate() {
for (pi, &(pr, pc)) in points.iter().enumerate() {
if i == pr && !tracked[pi].2 {
tracked[pi] = (logical.len(), current.len() + pc, true);
}
}
let soft = row.last().is_some_and(|c| c.is_wrapline());
let base = current.len();
let (cells, comb, links, ucolors) = row.into_parts();
for (col, marks) in comb {
if cells[col].is_combined() {
current_comb.insert(base + col, marks);
}
}
for (col, link) in links {
if cells[col].is_linked() {
current_links.insert(base + col, link);
}
}
for (col, color) in ucolors {
if cells[col].is_ucolored() {
current_ucolors.insert(base + col, color);
}
}
if soft {
let mut cells = cells;
if cells.last().is_some_and(Cell::is_leading_spacer) {
cells.pop();
}
current.extend(cells.into_iter().map(|mut c| {
c.remove_flags(CellFlags::WRAPLINE);
c
}));
} else {
let mut cells = cells;
while cells.last() == Some(&Cell::default()) {
cells.pop();
}
current.extend(cells);
logical.push(std::mem::take(&mut current));
logical_comb.push(std::mem::take(&mut current_comb));
logical_links.push(std::mem::take(&mut current_links));
logical_ucolors.push(std::mem::take(&mut current_ucolors));
}
}
if !current.is_empty() {
logical.push(current);
logical_comb.push(current_comb);
logical_links.push(current_links);
logical_ucolors.push(current_ucolors);
}
while logical.last().is_some_and(|l| l.is_empty()) {
logical.pop();
logical_comb.pop();
logical_links.pop();
logical_ucolors.pop();
}
let mut out: Vec<Row> = Vec::new();
let mut new_points = vec![(0usize, 0usize); points.len()];
for (li, line) in logical.iter().enumerate() {
let comb = &logical_comb[li];
let links = &logical_links[li];
let ucolors = &logical_ucolors[li];
let start = out.len();
if line.is_empty() {
out.push(Row::blank(new_cols));
} else {
let mut i = 0;
while i < line.len() {
let mut take = (line.len() - i).min(new_cols);
if i + take < line.len() && line[i + take - 1].is_wide() {
take -= 1;
}
let take = take.max(1); let seg_comb: Combining = comb
.range(i..i + take)
.map(|(&col, marks)| (col - i, marks.clone()))
.collect();
let seg_links: Links = links
.range(i..i + take)
.map(|(&col, &link)| (col - i, link))
.collect();
let seg_ucolors: UColors = ucolors
.range(i..i + take)
.map(|(&col, &color)| (col - i, color))
.collect();
let mut row =
Row::new(line[i..i + take].to_vec(), seg_comb, seg_links, seg_ucolors);
row.resize(new_cols);
i += take;
if i < line.len() {
row[new_cols - 1].insert_flags(CellFlags::WRAPLINE);
}
out.push(row);
}
}
for (pi, &(pl, poff, _)) in tracked.iter().enumerate() {
if pl == li {
let off = poff.min(line.len());
new_points[pi] = (start + off / new_cols, off % new_cols);
}
}
}
for (pi, &(pl, _, _)) in tracked.iter().enumerate() {
if pl >= logical.len() {
new_points[pi] = (out.len().saturating_sub(1), 0);
}
}
(out, new_points)
}
#[derive(Clone, Debug)]
pub struct Grid {
cols: usize,
rows: usize,
lines: Vec<Row>,
}
impl Grid {
pub fn new(cols: usize, rows: usize) -> Self {
let lines = vec![Row::blank(cols); rows];
Grid { cols, rows, lines }
}
pub fn cols(&self) -> usize {
self.cols
}
pub fn rows(&self) -> usize {
self.rows
}
pub fn cell(&self, row: usize, col: usize) -> &Cell {
&self.lines[row][col]
}
pub fn cell_mut(&mut self, row: usize, col: usize) -> &mut Cell {
&mut self.lines[row][col]
}
pub fn row(&self, row: usize) -> &[Cell] {
&self.lines[row]
}
pub(crate) fn row_ref(&self, row: usize) -> &Row {
&self.lines[row]
}
pub(crate) fn row_mut(&mut self, row: usize) -> &mut Row {
&mut self.lines[row]
}
pub(crate) fn row_owned(&self, row: usize) -> Row {
self.lines[row].clone()
}
pub fn scroll_up_region(&mut self, top: usize, bottom: usize) {
self.lines[top..=bottom].rotate_left(1);
for cell in self.lines[bottom].iter_mut() {
cell.reset();
}
}
pub(crate) fn scroll_up_recycle(&mut self, mut blank: Row) -> Row {
blank.clear(); blank.resize(self.cols);
self.lines.rotate_left(1); let last = self.rows - 1;
std::mem::replace(&mut self.lines[last], blank)
}
pub(crate) fn take_lines(&mut self) -> Vec<Row> {
std::mem::take(&mut self.lines)
}
pub(crate) fn set_screen(&mut self, mut lines: Vec<Row>, cols: usize, rows: usize) {
for row in &mut lines {
row.resize(cols);
}
while lines.len() < rows {
lines.push(Row::blank(cols));
}
lines.truncate(rows);
self.lines = lines;
self.cols = cols;
self.rows = rows;
}
pub fn clear(&mut self) {
for row in &mut self.lines {
for cell in row.iter_mut() {
cell.reset();
}
}
}
pub fn scroll_down_region(&mut self, top: usize, bottom: usize) {
self.lines[top..=bottom].rotate_right(1);
for cell in self.lines[top].iter_mut() {
cell.reset();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn stamped(cols: usize, rows: usize) -> Grid {
let mut g = Grid::new(cols, rows);
for r in 0..rows {
g.cell_mut(r, 0).set_c(char::from(b'a' + r as u8));
}
g
}
fn col0(g: &Grid) -> String {
(0..g.rows()).map(|r| g.cell(r, 0).c()).collect()
}
#[test]
fn full_screen_scroll_up_shifts_content_and_blanks_bottom() {
let mut g = stamped(2, 3); g.scroll_up_region(0, 2);
assert_eq!(col0(&g), "bc "); }
#[test]
fn full_screen_scroll_down_shifts_content_and_blanks_top() {
let mut g = stamped(2, 3); g.scroll_down_region(0, 2);
assert_eq!(col0(&g), " ab");
}
#[test]
fn sub_region_scroll_leaves_rows_outside_the_region_untouched() {
let mut g = stamped(2, 4); g.scroll_up_region(0, 1); assert_eq!(col0(&g), "b cd");
}
#[test]
fn scroll_up_recycle_moves_out_row0_and_blanks_a_dirty_recycled_row() {
let mut g = stamped(2, 3); let mut x = Cell::default();
x.set_c('X');
let dirty = Row::from_cells(vec![x; 2]);
let evicted = g.scroll_up_recycle(dirty);
assert_eq!(evicted[0].c(), 'a'); assert_eq!(col0(&g), "bc "); }
#[test]
fn take_lines_returns_rows_in_logical_order_after_a_scroll() {
let mut g = stamped(1, 3); g.scroll_up_region(0, 2); let lines = g.take_lines();
let got: String = lines.iter().map(|r| r[0].c()).collect();
assert_eq!(got, "bc ");
}
}