tastty-core 0.1.0

Sans-IO core of the tastty terminal session library: VT parser, screen buffer, and byte encoders.
//! Wrap-preserving resize reflow for the primary buffer.
//!
//! [`reflow_primary`] walks the primary buffer's logical lines via the
//! shared [`super::logical_line`] iterator and re-emits physical rows
//! under the new column count, preserving wrap flags so a logical line
//! that needed three soft-wrapped rows at 80 cols becomes a five-row
//! chain at 40 cols rather than fragmenting into three independent
//! rows. Rows that fall above the new drawing region spill into
//! scrollback (bumping `pushed_to_scrollback` so the
//! [`AbsolutePosition`](super::AbsolutePosition) coordinate space stays
//! monotonic). The cursor is re-anchored to the same logical character
//! it pointed at before the resize.
//!
//! The alternate-screen buffer is not reflowed: full-screen programs
//! repaint on SIGWINCH, so a transient mid-resize reflow would only
//! produce visible artifacts before that repaint arrives.

use super::Screen;
use super::logical_line::{self, LogicalLineOptions};
use crate::cell::Cell;
use crate::grid::{Pos, Size};
use crate::row::Row;

pub(super) fn reflow_primary(screen: &mut Screen, new_size: Size) {
    let old_size = screen.grid.size();
    if old_size == new_size {
        return;
    }

    let cursor = screen.grid.pos();
    let scrollback_count = screen.grid.scrollback_available();
    let cursor_global_row = scrollback_count + cursor.row as usize;
    let cursor_global_col = cursor.col as usize;

    let mut materialized: Vec<MaterializedLine> = Vec::new();
    let mut anchor_line_idx: Option<usize> = None;
    let mut anchor_offset: usize = 0;

    {
        let opts = LogicalLineOptions {
            include_scrollback: true,
            join_wrapped: true,
            trim_trailing_blanks: false,
        };
        let mut consumed_rows: usize = 0;
        for span in logical_line::iter_grid(&screen.grid, opts) {
            let n_rows = span.rows.len();
            let mut cells: Vec<Cell> = Vec::with_capacity(n_rows * usize::from(old_size.cols));
            for row in &span.rows {
                for c in 0..old_size.cols {
                    cells.push(row.get(c).cloned().unwrap_or_else(Cell::new));
                }
            }

            if anchor_line_idx.is_none()
                && cursor_global_row >= consumed_rows
                && cursor_global_row < consumed_rows + n_rows
            {
                let row_in_line = cursor_global_row - consumed_rows;
                anchor_line_idx = Some(materialized.len());
                anchor_offset = row_in_line * usize::from(old_size.cols) + cursor_global_col;
            }
            consumed_rows += n_rows;
            materialized.push(MaterializedLine { cells });
        }
    }

    // Trailing blank lines must not pin themselves in the buffer: a shrink
    // that needs an extra physical row for the content line would otherwise
    // spill that content above the drawing region. The cursor's own line
    // stays even when blank, so a cursor below the last typed character
    // keeps its anchor.
    while materialized
        .last()
        .is_some_and(|line| line.cells.iter().all(|c| !c.has_contents()))
        && anchor_line_idx != Some(materialized.len() - 1)
    {
        materialized.pop();
    }

    let new_cols = usize::from(new_size.cols);
    let mut emitted: Vec<Row> = Vec::new();
    let mut new_cursor_global_idx: Option<usize> = None;
    let mut new_cursor_col: u16 = 0;

    for (line_idx, line) in materialized.iter().enumerate() {
        let trimmed_len = line
            .cells
            .iter()
            .rposition(Cell::has_contents)
            .map(|p| p + 1)
            .unwrap_or(0);

        let cursor_offset = if anchor_line_idx == Some(line_idx) {
            Some(anchor_offset)
        } else {
            None
        };

        let effective_len = match cursor_offset {
            Some(off) => trimmed_len.max(off + 1),
            None => trimmed_len,
        };

        let num_rows = if effective_len == 0 {
            1
        } else {
            effective_len.div_ceil(new_cols.max(1))
        };

        let line_start_idx = emitted.len();

        for chunk_idx in 0..num_rows {
            let mut new_row = Row::new(new_size.cols);
            new_row.mark_dirty();
            let chunk_start = chunk_idx * new_cols;
            let chunk_end = ((chunk_idx + 1) * new_cols).min(line.cells.len());
            for (col_idx, src_idx) in (chunk_start..chunk_end).enumerate() {
                if let Some(slot) = new_row.get_mut(col_idx as u16) {
                    *slot = line.cells[src_idx].clone();
                }
            }
            new_row.wrap(chunk_idx + 1 < num_rows);
            emitted.push(new_row);
        }

        if let Some(off) = cursor_offset {
            let chunk_idx = off / new_cols.max(1);
            let col_in_chunk = off % new_cols.max(1);
            new_cursor_global_idx = Some(line_start_idx + chunk_idx);
            new_cursor_col = col_in_chunk as u16;
        }
    }

    let new_drawing_count = usize::from(new_size.rows);
    let total_emitted = emitted.len();
    let drawing_start = total_emitted.saturating_sub(new_drawing_count);

    let mut drawing_rows = emitted.split_off(drawing_start);
    let scrollback_extra = emitted;

    while drawing_rows.len() < new_drawing_count {
        let mut blank = Row::new(new_size.cols);
        blank.mark_dirty();
        drawing_rows.push(blank);
    }

    let new_pos = match new_cursor_global_idx {
        Some(g) if g >= drawing_start => Pos {
            row: (g - drawing_start) as u16,
            col: new_cursor_col,
        },
        _ => Pos { row: 0, col: 0 },
    };

    screen
        .grid
        .install_reflowed(drawing_rows, scrollback_extra, new_size, new_pos);
}

struct MaterializedLine {
    cells: Vec<Cell>,
}