use super::{Buffer, Cell, Rect, Style};
pub struct RenderContext<'a> {
area: Rect,
buf: &'a mut Buffer,
scroll: (u16, u16),
}
impl<'a> RenderContext<'a> {
pub fn new(area: Rect, buf: &'a mut Buffer, scroll: (u16, u16)) -> Self {
Self { area, buf, scroll }
}
pub fn area(&self) -> Rect {
self.area
}
pub fn scroll(&self) -> (u16, u16) {
self.scroll
}
pub fn width(&self) -> u16 {
self.area.width
}
pub fn height(&self) -> u16 {
self.area.height
}
fn to_grid(&self, rel_x: u16, rel_y: u16) -> Option<(u16, u16)> {
if rel_x >= self.area.width || rel_y >= self.area.height {
return None;
}
Some((
self.area.x.saturating_add(rel_x),
self.area.y.saturating_add(rel_y),
))
}
pub fn set_cell(&mut self, rel_x: u16, rel_y: u16, cell: Cell) {
if let Some((x, y)) = self.to_grid(rel_x, rel_y)
&& let Some(slot) = self.buf.cell_mut(x, y)
{
*slot = cell;
}
}
pub fn set_char(&mut self, rel_x: u16, rel_y: u16, ch: char, style: Style) {
if let Some((x, y)) = self.to_grid(rel_x, rel_y) {
self.buf.set_char(x, y, ch, style);
}
}
pub fn set_string(&mut self, rel_x: u16, rel_y: u16, s: &str, style: Style) -> (u16, u16) {
if rel_y >= self.area.height || rel_x >= self.area.width {
return (rel_x, rel_y);
}
let x = self.area.x.saturating_add(rel_x);
let y = self.area.y.saturating_add(rel_y);
let max = self.area.width.saturating_sub(rel_x);
let (end_x, _) = self.buf.set_stringn(x, y, s, max, style);
(end_x.saturating_sub(self.area.x), rel_y)
}
pub fn fill(&mut self, rel_area: Rect, cell: Cell) {
let abs = Rect::new(
self.area.x.saturating_add(rel_area.x),
self.area.y.saturating_add(rel_area.y),
rel_area.width,
rel_area.height,
);
self.buf.fill(self.area.intersection(abs), cell);
}
pub fn set_style(&mut self, rel_x: u16, rel_y: u16, style: Style) {
if let Some((x, y)) = self.to_grid(rel_x, rel_y) {
self.buf.set_style(x, y, style);
}
}
pub fn buffer(&self) -> &Buffer {
self.buf
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::{Color, Modifier};
fn buf() -> Buffer {
Buffer::empty(Rect::new(0, 0, 10, 5))
}
#[test]
fn area_and_scroll_expose_input() {
let mut b = buf();
let ctx = RenderContext::new(Rect::new(2, 1, 5, 3), &mut b, (4, 7));
assert_eq!(ctx.area(), Rect::new(2, 1, 5, 3));
assert_eq!(ctx.scroll(), (4, 7));
assert_eq!(ctx.width(), 5);
assert_eq!(ctx.height(), 3);
}
#[test]
fn set_char_relative_to_area_origin() {
let mut b = buf();
{
let mut ctx = RenderContext::new(Rect::new(2, 1, 5, 3), &mut b, (0, 0));
ctx.set_char(0, 0, 'A', Style::new().fg(Color::Rgb(255, 0, 0)));
}
assert_eq!(b.cell(2, 1).unwrap().symbol(), "A");
assert_eq!(b.cell(2, 1).unwrap().fg, Color::Rgb(255, 0, 0));
assert_eq!(b.cell(0, 0).unwrap().symbol(), " ");
assert_eq!(b.cell(1, 1).unwrap().symbol(), " ");
}
#[test]
fn set_char_out_of_area_is_noop() {
let mut b = buf();
{
let mut ctx = RenderContext::new(Rect::new(2, 1, 3, 2), &mut b, (0, 0));
ctx.set_char(3, 0, 'X', Style::new());
ctx.set_char(0, 2, 'Y', Style::new());
}
assert_eq!(b.cell(5, 1).unwrap().symbol(), " ");
assert_eq!(b.cell(2, 3).unwrap().symbol(), " ");
}
#[test]
fn set_string_clips_at_area_right_edge() {
let mut b = buf();
{
let mut ctx = RenderContext::new(Rect::new(0, 0, 5, 1), &mut b, (0, 0));
let end = ctx.set_string(0, 0, "helloworld", Style::new());
assert_eq!(end, (5, 0));
}
for x in 0..5 {
assert_eq!(
b.cell(x, 0).unwrap().symbol(),
&("hello"[x as usize..x as usize + 1])
);
}
assert_eq!(b.cell(5, 0).unwrap().symbol(), " ");
}
#[test]
fn set_string_cjk_width_respected() {
let mut b = buf();
{
let mut ctx = RenderContext::new(Rect::new(0, 0, 3, 1), &mut b, (0, 0));
let end = ctx.set_string(0, 0, "中国", Style::new());
assert_eq!(end, (3, 0));
}
assert_eq!(b.cell(0, 0).unwrap().symbol(), "中");
assert!(b.cell(1, 0).unwrap().is_spacer());
assert_eq!(b.cell(2, 0).unwrap().symbol(), "…");
}
#[test]
fn fill_clamps_to_area() {
let mut b = buf();
{
let mut ctx = RenderContext::new(Rect::new(2, 1, 3, 2), &mut b, (0, 0));
ctx.fill(Rect::new(0, 0, 100, 100), Cell::new("#"));
}
for y in 1..3 {
for x in 2..5 {
assert_eq!(b.cell(x, y).unwrap().symbol(), "#");
}
}
assert_eq!(b.cell(0, 0).unwrap().symbol(), " ");
assert_eq!(b.cell(5, 1).unwrap().symbol(), " ");
assert_eq!(b.cell(2, 3).unwrap().symbol(), " ");
}
#[test]
fn set_cell_composes_everything() {
let mut b = buf();
let mut my_cell = Cell::new("Z");
my_cell.fg = Color::Rgb(0, 0, 255);
my_cell.bg = Color::Rgb(255, 0, 0);
my_cell.modifier = Modifier::BOLD;
{
let mut ctx = RenderContext::new(Rect::new(1, 1, 3, 3), &mut b, (0, 0));
ctx.set_cell(1, 1, my_cell.clone());
}
let placed = b.cell(2, 2).unwrap();
assert_eq!(placed.symbol(), "Z");
assert_eq!(placed.fg, Color::Rgb(0, 0, 255));
assert_eq!(placed.bg, Color::Rgb(255, 0, 0));
assert_eq!(placed.modifier, Modifier::BOLD);
}
#[test]
fn set_style_preserves_symbol() {
let mut b = buf();
b.set_char(3, 2, 'X', Style::new().fg(Color::Rgb(255, 255, 255)));
{
let mut ctx = RenderContext::new(Rect::new(3, 2, 1, 1), &mut b, (0, 0));
ctx.set_style(0, 0, Style::new().fg(Color::Rgb(255, 0, 0)));
}
let c = b.cell(3, 2).unwrap();
assert_eq!(c.symbol(), "X"); assert_eq!(c.fg, Color::Rgb(255, 0, 0)); }
#[test]
fn elements_cannot_paint_outside_their_rect() {
let mut b = buf();
{
let mut ctx = RenderContext::new(Rect::new(2, 1, 3, 2), &mut b, (0, 0));
ctx.set_char(100, 100, '!', Style::new());
ctx.set_string(50, 50, "escape", Style::new());
}
let blank = buf();
for y in 0..5 {
for x in 0..10 {
if !((2..5).contains(&x) && (1..3).contains(&y)) {
assert_eq!(b.cell(x, y).unwrap(), blank.cell(x, y).unwrap());
}
}
}
}
#[test]
fn buffer_accessor_is_readable() {
let mut b = buf();
b.set_char(0, 0, 'Q', Style::new());
let ctx = RenderContext::new(Rect::new(2, 2, 3, 3), &mut b, (0, 0));
let buffer_snapshot = ctx.buffer();
assert_eq!(buffer_snapshot.cell(0, 0).unwrap().symbol(), "Q");
}
}