use std::{fmt::Display, io::Write};
use crossterm::{
cursor::MoveTo,
style::Color,
terminal::{Clear, ClearType},
};
use crate::graphics::{Block, FontStyle, TermLayout, styled_string};
impl Display for TermLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let cols = self.columns as usize;
let rows = self.rows as usize;
let sy = self.scroll_y as usize;
for r in 0..rows {
let start = sy
.saturating_add(r)
.saturating_mul(cols)
.min(self.layout.len());
let end = start.saturating_add(cols).min(self.layout.len());
for cell in self.layout[start..end].iter() {
if cell.word != " " {
write!(f, "{}", cell)?;
}
}
}
write!(f, "")
}
}
impl TermLayout {
pub fn apply_text_selection(&mut self, lines: &[(u32, u32, u32)], selection_bg: Color) {
let cols = self.columns as usize;
let max = self.layout.len();
for &(y, xs, xe) in lines {
let row_start = (y as usize).saturating_mul(cols);
if row_start >= max {
continue;
}
let xe = (xe as usize).min(cols);
for x in (xs as usize)..xe {
let idx = row_start.saturating_add(x);
if idx >= max {
break;
}
self.layout[idx].bcolor =
Some(selection_blend(self.layout[idx].bcolor, selection_bg));
}
}
}
pub fn render(&mut self, out: &mut impl Write) {
let bytes = self.render_diff_to_bytes();
if bytes.is_empty() {
return;
}
if let Err(e) = out.write_all(&bytes) {
log::error!("Failed to write rendered frame: {}", e);
}
}
pub fn render_to_bytes(&self) -> Vec<u8> {
let mut out: Vec<u8> = Vec::with_capacity(
(self.rows as usize)
.saturating_mul(self.columns as usize)
.saturating_mul(4),
);
let cols = self.columns as usize;
let (scroll_start, scroll_end) = self.viewport_range();
let mut batch = String::new();
let mut i = scroll_start;
while let Some(run) = self.next_batch(i, scroll_end, cols, &mut batch) {
self.queue_batch(&mut out, &run, &batch);
i = run.end;
}
out
}
pub fn render_diff_to_bytes(&mut self) -> Vec<u8> {
let mut out: Vec<u8> = Vec::with_capacity(
(self.rows as usize)
.saturating_mul(self.columns as usize)
.saturating_mul(4),
);
let cols = self.columns as usize;
let (scroll_start, scroll_end) = self.viewport_range();
let viewport_cells = scroll_end.saturating_sub(scroll_start);
if self.last_frame.len() != viewport_cells {
if let Err(e) = crossterm::queue!(out, Clear(ClearType::All), MoveTo(0, 0)) {
log::error!("Failed to queue initial screen clear: {}", e);
}
self.last_frame = vec![Block::default(); viewport_cells];
}
let mut batch = String::new();
let mut i = scroll_start;
while let Some(run) = self.next_batch(i, scroll_end, cols, &mut batch) {
let changed = (run.start..run.end).any(|i| {
!self.layout[i].word.is_empty()
&& !block_state_eq(&self.last_frame[i - scroll_start], &self.layout[i])
});
if changed {
self.queue_batch(&mut out, &run, &batch);
}
i = run.end;
}
for (snapshot, cell) in self
.last_frame
.iter_mut()
.zip(self.layout[scroll_start..scroll_end].iter())
{
if !block_state_eq(snapshot, cell) {
snapshot.word.clear();
snapshot.word.push_str(&cell.word);
snapshot.fcolor = cell.fcolor;
snapshot.bcolor = cell.bcolor;
snapshot.style = cell.style;
}
}
out
}
fn viewport_range(&self) -> (usize, usize) {
let cols = self.columns as usize;
let scroll_start = (self.scroll_y as usize)
.saturating_mul(cols)
.min(self.layout.len());
let scroll_end = scroll_start
.saturating_add((self.rows as usize).saturating_mul(cols))
.min(self.layout.len());
(scroll_start, scroll_end)
}
fn next_batch(
&self,
from: usize,
scroll_end: usize,
cols: usize,
batch: &mut String,
) -> Option<BatchRun> {
let mut i = from;
while i < scroll_end && self.layout[i].word.is_empty() {
i += 1;
}
if i >= scroll_end {
return None;
}
let start = i;
let cell = &self.layout[start];
let fg = cell.fcolor;
let bg = cell.bcolor;
let style = cell.style;
let bare = is_rendered_as_bare_space(cell);
batch.clear();
batch.push_str(&cell.word);
i += 1;
while i < scroll_end {
let c = &self.layout[i];
if c.word.is_empty() {
i += 1;
continue;
}
if i.is_multiple_of(cols) {
break;
}
let c_bare = is_rendered_as_bare_space(c);
if c_bare != bare {
break;
}
if !c_bare && (c.fcolor != fg || c.bcolor != bg || c.style != style) {
break;
}
batch.push_str(&c.word);
i += 1;
}
Some(BatchRun {
start,
end: i,
fg,
bg,
style,
bare,
})
}
fn queue_batch(&self, out: &mut Vec<u8>, run: &BatchRun, batch: &str) {
let cols = self.columns as usize;
let batch_row = run.start / cols;
let x = (run.start % cols) as u16;
let y = batch_row.saturating_sub(self.scroll_y as usize);
if batch_row >= self.scroll_y as usize && y < self.rows as usize {
if let Err(e) = crossterm::queue!(out, MoveTo(x, y as u16)) {
log::error!("Failed to queue cursor move to ({}, {}): {}", x, y, e);
}
if run.bare {
out.extend_from_slice(batch.as_bytes());
} else {
push_styled(out, batch, run.style, run.fg, run.bg);
}
}
}
}
struct BatchRun {
start: usize,
end: usize,
fg: Option<Color>,
bg: Option<Color>,
style: FontStyle,
bare: bool,
}
fn block_state_eq(a: &Block, b: &Block) -> bool {
a.word == b.word && a.fcolor == b.fcolor && a.bcolor == b.bcolor && a.style == b.style
}
fn push_styled(
out: &mut Vec<u8>,
text: &str,
style: FontStyle,
fg: Option<Color>,
bg: Option<Color>,
) {
if let Err(e) = write!(out, "{}", styled_string(text, style, fg, bg)) {
log::error!("Failed to write styled run: {}", e);
}
}
pub fn is_rendered_as_bare_space(cell: &Block) -> bool {
cell.word == " " && cell.bcolor.is_none() && cell.style.is_empty()
}
fn selection_blend(existing: Option<Color>, selection_bg: Color) -> Color {
let (
Some(Color::Rgb { r, g, b }),
Color::Rgb {
r: sr,
g: sg,
b: sb,
},
) = (existing, selection_bg)
else {
return selection_bg;
};
Color::Rgb {
r: ((r as u16 + sr as u16) / 2) as u8,
g: ((g as u16 + sg as u16) / 2) as u8,
b: ((b as u16 + sb as u16) / 2) as u8,
}
}