use alloc::{
format,
rc::Rc,
string::{String, ToString},
};
use core::cell::RefCell;
use ratatui_core::{
buffer::Buffer,
layout::{Offset, Position, Positions, Rect},
style::{Color, Modifier, Style},
};
pub trait BufferRenderer {
fn render_buffer(&self, offset: Offset, buf: &mut Buffer);
fn render_buffer_region(&self, src_region: Rect, offset: Offset, buf: &mut Buffer);
}
impl BufferRenderer for Rc<RefCell<Buffer>> {
fn render_buffer(&self, offset: Offset, buf: &mut Buffer) {
(*self.as_ref().borrow()).render_buffer(offset, buf);
}
fn render_buffer_region(&self, src_region: Rect, offset: Offset, buf: &mut Buffer) {
(*self.as_ref().borrow()).render_buffer_region(src_region, offset, buf);
}
}
#[cfg(feature = "sendable")]
impl BufferRenderer for crate::RefCount<Buffer> {
fn render_buffer(&self, offset: Offset, buf: &mut Buffer) {
(*self.lock().unwrap()).render_buffer(offset, buf);
}
fn render_buffer_region(&self, src_region: Rect, offset: Offset, buf: &mut Buffer) {
(*self.lock().unwrap()).render_buffer_region(src_region, offset, buf);
}
}
impl BufferRenderer for Buffer {
fn render_buffer(&self, offset: Offset, buf: &mut Buffer) {
blit_buffer(self, buf, offset);
}
fn render_buffer_region(&self, src_region: Rect, offset: Offset, buf: &mut Buffer) {
blit_buffer_region(self, src_region, buf, offset);
}
}
pub fn blit_buffer(src: &Buffer, dst: &mut Buffer, offset: Offset) {
blit_buffer_region(src, src.area, dst, offset);
}
pub fn blit_buffer_region(src: &Buffer, src_region: Rect, dst: &mut Buffer, offset: Offset) {
let src_region = src_region.intersection(src.area);
let clip = ClipRegion::new(src_region, *dst.area(), offset);
if !clip.is_valid() {
return; }
for p in clip.normalized_positions() {
let src_cell = &src[clip.src_pos(p)];
dst[clip.dst_pos(p)] = src_cell.clone();
}
}
#[deprecated(
since = "0.16.0",
note = "use `buffer_to_ansi_string(buffer, false)` instead"
)]
pub fn render_as_ansi_string(buffer: &Buffer) -> String {
buffer_to_ansi_string(buffer, false)
}
pub fn buffer_to_ansi_string(buffer: &Buffer, include_all_cells: bool) -> String {
use unicode_width::UnicodeWidthStr;
let mut s = String::new();
let mut style = Style::default();
for y in 0..buffer.area.height {
let mut x = 0;
while x < buffer.area.width {
let cell = buffer.cell(Position::new(x, y)).unwrap();
if !include_all_cells && cell.symbol() == " " && x > 0 {
if let Some(prev_cell) = buffer.cell(Position::new(x - 1, y)) {
if prev_cell.symbol().width() > 1 {
x += 1;
continue;
}
}
}
if cell.style() != style {
s.push_str("\x1b[0m"); s.push_str(&escape_code_of(cell.style()));
style = cell.style();
}
s.push_str(cell.symbol());
x += 1;
}
s.push_str("\x1b[0m");
s.push('\n');
style = Style::default();
}
s
}
fn escape_code_of(style: Style) -> String {
let mut result = String::new();
if let Some(color) = style.fg {
if color != Color::Reset {
result.push_str(&color_code(color, true));
}
}
if let Some(color) = style.bg {
if color != Color::Reset {
result.push_str(&color_code(color, false));
}
}
if style.add_modifier.contains(Modifier::BOLD) {
result.push_str("\x1b[1m");
}
if style.add_modifier.contains(Modifier::DIM) {
result.push_str("\x1b[2m");
}
if style.add_modifier.contains(Modifier::ITALIC) {
result.push_str("\x1b[3m");
}
if style.add_modifier.contains(Modifier::UNDERLINED) {
result.push_str("\x1b[4m");
}
if style.add_modifier.contains(Modifier::SLOW_BLINK) {
result.push_str("\x1b[5m");
}
if style.add_modifier.contains(Modifier::RAPID_BLINK) {
result.push_str("\x1b[6m");
}
if style.add_modifier.contains(Modifier::REVERSED) {
result.push_str("\x1b[7m");
}
if style.add_modifier.contains(Modifier::HIDDEN) {
result.push_str("\x1b[8m");
}
if style.add_modifier.contains(Modifier::CROSSED_OUT) {
result.push_str("\x1b[9m");
}
result
}
fn color_code(color: Color, foreground: bool) -> String {
let base = if foreground { 38 } else { 48 };
match color {
Color::Reset => "\x1b[0m".to_string(),
Color::Black => format!("\x1b[{base};5;0m"),
Color::Red => format!("\x1b[{base};5;1m"),
Color::Green => format!("\x1b[{base};5;2m"),
Color::Yellow => format!("\x1b[{base};5;3m"),
Color::Blue => format!("\x1b[{base};5;4m"),
Color::Magenta => format!("\x1b[{base};5;5m"),
Color::Cyan => format!("\x1b[{base};5;6m"),
Color::Gray => format!("\x1b[{base};5;7m"),
Color::DarkGray => format!("\x1b[{base};5;8m"),
Color::LightRed => format!("\x1b[{base};5;9m"),
Color::LightGreen => format!("\x1b[{base};5;10m"),
Color::LightYellow => format!("\x1b[{base};5;11m"),
Color::LightBlue => format!("\x1b[{base};5;12m"),
Color::LightMagenta => format!("\x1b[{base};5;13m"),
Color::LightCyan => format!("\x1b[{base};5;14m"),
Color::White => format!("\x1b[{base};5;15m"),
Color::Indexed(i) => format!("\x1b[{base};5;{i}m"),
Color::Rgb(r, g, b) => format!("\x1b[{base};2;{r};{g};{b}m"),
}
}
struct ClipRegion {
src: Rect,
dst: Rect,
}
impl ClipRegion {
fn new(src_region: Rect, dst_bounds: Rect, dst_offset: Offset) -> Self {
let x_offset = dst_offset.x.min(0).unsigned_abs() as u16;
let y_offset = dst_offset.y.min(0).unsigned_abs() as u16;
let dst = Rect::new(
dst_offset.x.max(0) as u16,
dst_offset.y.max(0) as u16,
src_region.width,
src_region.height,
);
let width = (dst.width - x_offset)
.min(dst_bounds.width.saturating_sub(dst.x))
.min(src_region.width);
let height = (dst.height - y_offset)
.min(dst_bounds.height.saturating_sub(dst.y))
.min(src_region.height);
Self {
src: Rect::new(
src_region.x + x_offset,
src_region.y + y_offset,
width,
height,
),
dst: Rect::new(dst.x, dst.y, width, height),
}
}
fn is_valid(&self) -> bool {
self.src.area() > 0
}
fn width(&self) -> u16 {
self.src.width
}
fn height(&self) -> u16 {
self.src.height
}
fn normalized_positions(&self) -> Positions {
Rect::new(0, 0, self.width(), self.height()).positions()
}
fn src_pos(&self, pos: Position) -> Position {
Position::new(self.src.x + pos.x, self.src.y + pos.y)
}
fn dst_pos(&self, pos: Position) -> Position {
Position::new(self.dst.x + pos.x, self.dst.y + pos.y)
}
}
#[cfg(test)]
mod tests {
use ratatui_core::buffer::Buffer;
use super::*;
use crate::ref_count;
fn assert_buffer_to_buffer_copy(offset: Offset, expected: &Buffer) {
let aux_buffer = ref_count(Buffer::with_lines(["abcd", "efgh", "ijkl", "mnop"]));
let mut buf = Buffer::with_lines([
". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ",
". . . . ",
]);
aux_buffer.render_buffer(offset, &mut buf);
assert_eq!(&buf, expected);
}
#[test]
fn test_render_offsets_in_bounds() {
assert_buffer_to_buffer_copy(
Offset { x: 0, y: 0 },
&Buffer::with_lines([
"abcd. . ", "efgh. . ", "ijkl. . ", "mnop. . ", ". . . . ", ". . . . ", ". . . . ",
". . . . ",
]),
);
assert_buffer_to_buffer_copy(
Offset { x: 4, y: 3 },
&Buffer::with_lines([
". . . . ", ". . . . ", ". . . . ", ". . abcd", ". . efgh", ". . ijkl", ". . mnop",
". . . . ",
]),
);
}
#[test]
fn test_render_offsets_out_of_bounds() {
assert_buffer_to_buffer_copy(
Offset { x: -1, y: -2 },
&Buffer::with_lines([
"jkl . . ", "nop . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ",
". . . . ",
]),
);
assert_buffer_to_buffer_copy(
Offset { x: 6, y: 6 },
&Buffer::with_lines([
". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . ab",
". . . ef",
]),
);
}
#[test]
fn test_render_from_larger_aux_buffer() {
let aux_buffer = ref_count(Buffer::with_lines([
"AAAAAAAAAA",
"BBBBBBBBBB",
"CCCCCCCCCC",
"DDDDDDDDDD",
"EEEEEEEEEE",
"FFFFFFFFFF",
]));
let buffer = || Buffer::with_lines([". . . . ", ". . . . ", ". . . . "]);
let mut buf = buffer();
aux_buffer.render_buffer(Offset::default(), &mut buf);
assert_eq!(
buf,
Buffer::with_lines(["AAAAAAAA", "BBBBBBBB", "CCCCCCCC",])
);
let mut buf = buffer();
aux_buffer.render_buffer(Offset { x: 0, y: 2 }, &mut buf);
assert_eq!(
buf,
Buffer::with_lines([". . . . ", ". . . . ", "AAAAAAAA",])
);
let mut buf = buffer();
aux_buffer.render_buffer(Offset { x: 0, y: -2 }, &mut buf);
assert_eq!(
buf,
Buffer::with_lines(["CCCCCCCC", "DDDDDDDD", "EEEEEEEE",])
);
let mut buf = buffer();
aux_buffer.render_buffer(Offset { x: 2, y: 1 }, &mut buf);
assert_eq!(
buf,
Buffer::with_lines([". . . . ", ". AAAAAA", ". BBBBBB",])
);
let mut buf = buffer();
aux_buffer.render_buffer(Offset { x: 0, y: 6 }, &mut buf);
assert_eq!(
buf,
Buffer::with_lines([". . . . ", ". . . . ", ". . . . ",])
);
let mut buf = buffer();
aux_buffer.render_buffer(Offset { x: -5, y: -5 }, &mut buf);
assert_eq!(
buf,
Buffer::with_lines(["FFFFF . ", ". . . . ", ". . . . ",])
);
}
#[test]
fn test_buffer_to_ansi_string_unicode() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
buffer.set_stringn(0, 0, "🦀test", 8, Style::default());
let ansi_output = buffer_to_ansi_string(&buffer, false);
assert!(ansi_output.contains("🦀test"));
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
buffer.set_stringn(0, 0, "世界test", 8, Style::default());
let ansi_output = buffer_to_ansi_string(&buffer, false);
assert!(ansi_output.contains("世界test"));
let mut buffer = Buffer::empty(Rect::new(0, 0, 6, 1));
buffer.set_stringn(0, 0, "🦀", 2, Style::default().fg(Color::Red));
buffer.set_stringn(2, 0, "test", 4, Style::default().fg(Color::Blue));
let ansi_output = buffer_to_ansi_string(&buffer, false);
assert!(ansi_output.contains("🦀"));
assert!(ansi_output.contains("test"));
assert!(ansi_output.contains("\x1b[38;5;1m")); assert!(ansi_output.contains("\x1b[38;5;4m")); assert!(ansi_output.contains("\x1b[0m"));
let mut buffer = Buffer::empty(Rect::new(0, 0, 3, 1));
buffer.set_stringn(0, 0, "a🦀", 3, Style::default());
let ansi_output = buffer_to_ansi_string(&buffer, false);
assert!(ansi_output.contains("a🦀"));
assert!(!ansi_output.contains("a🦀 ")); }
#[test]
fn test_buffer_to_ansi_string_spacing_demo() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 12, 1));
buffer.set_stringn(0, 0, "🦀🐍🌟hello", 12, Style::default());
let ansi_output = buffer_to_ansi_string(&buffer, false);
assert!(ansi_output.contains("🦀🐍🌟hello"));
assert!(!ansi_output.contains("🦀 🐍")); assert!(!ansi_output.contains("🐍 🌟")); assert!(!ansi_output.contains("🌟 hello")); }
#[test]
fn test_buffer_to_ansi_string_include_all_cells() {
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
buffer.set_stringn(0, 0, "🦀test", 8, Style::default());
let ansi_output_default = buffer_to_ansi_string(&buffer, false);
assert!(ansi_output_default.contains("🦀test"));
let ansi_output_all_cells = buffer_to_ansi_string(&buffer, true);
assert!(ansi_output_all_cells.contains("🦀"));
assert!(ansi_output_all_cells.contains("test"));
let mut buffer = Buffer::empty(Rect::new(0, 0, 8, 1));
buffer.set_stringn(0, 0, "世界", 4, Style::default());
let ansi_output_default = buffer_to_ansi_string(&buffer, false);
let ansi_output_all_cells = buffer_to_ansi_string(&buffer, true);
assert!(ansi_output_default.contains("世界"));
assert!(ansi_output_all_cells.contains("世") || ansi_output_all_cells.contains("界"));
assert!(ansi_output_all_cells.len() >= ansi_output_default.len());
}
#[test]
fn test_blit_buffer_region() {
let buffer =
|| Buffer::with_lines([". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . "]);
let aux_buffer = Buffer::with_lines(["abcd", "efgh", "ijkl", "mnop"]);
let mut buf = buffer();
blit_buffer_region(
&aux_buffer,
Rect::new(1, 1, 2, 2),
&mut buf,
Offset::default(),
);
assert_eq!(
buf,
Buffer::with_lines(["fg. . . ", "jk. . . ", ". . . . ", ". . . . ", ". . . . ",])
);
let mut buf = buffer();
blit_buffer_region(&aux_buffer, Rect::new(1, 1, 2, 2), &mut buf, Offset {
x: 4,
y: 2,
});
assert_eq!(
buf,
Buffer::with_lines([". . . . ", ". . . . ", ". . fg. ", ". . jk. ", ". . . . ",])
);
let mut buf = buffer();
blit_buffer_region(&aux_buffer, Rect::new(1, 1, 3, 3), &mut buf, Offset {
x: -1,
y: -1,
});
assert_eq!(
buf,
Buffer::with_lines(["kl. . . ", "op. . . ", ". . . . ", ". . . . ", ". . . . ",])
);
let mut buf = buffer();
blit_buffer_region(
&aux_buffer,
Rect::new(2, 2, 3, 3),
&mut buf,
Offset::default(),
);
assert_eq!(
buf,
Buffer::with_lines(["kl. . . ", "op. . . ", ". . . . ", ". . . . ", ". . . . ",])
);
let mut buf = buffer();
blit_buffer_region(&aux_buffer, Rect::new(0, 0, 2, 2), &mut buf, Offset {
x: 6,
y: 3,
});
assert_eq!(
buf,
Buffer::with_lines([". . . . ", ". . . . ", ". . . . ", ". . . ab", ". . . ef",])
);
let mut buf = buffer();
blit_buffer_region(&aux_buffer, Rect::new(0, 0, 2, 2), &mut buf, Offset {
x: 8,
y: 8,
});
assert_eq!(
buf,
Buffer::with_lines([". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ",])
);
let mut buf = buffer();
blit_buffer_region(
&aux_buffer,
Rect::new(1, 1, 0, 0),
&mut buf,
Offset::default(),
);
assert_eq!(
buf,
Buffer::with_lines([". . . . ", ". . . . ", ". . . . ", ". . . . ", ". . . . ",])
);
let mut buf = buffer();
blit_buffer_region(
&aux_buffer,
Rect::new(0, 0, 4, 4),
&mut buf,
Offset::default(),
);
assert_eq!(
buf,
Buffer::with_lines(["abcd. . ", "efgh. . ", "ijkl. . ", "mnop. . ", ". . . . ",])
);
}
}