mod diff;
mod hitgrid;
mod threaded;
pub use diff::BufferDiff;
pub use hitgrid::HitGrid;
pub use threaded::{ThreadedRenderStats, ThreadedRenderer};
use crate::ansi::AnsiWriter;
use crate::buffer::{BoxOptions, BoxStyle, ClipRect, OptimizedBuffer, ScissorStack, TitleAlign};
use crate::color::Rgba;
use crate::link::LinkPool;
use crate::terminal::{CursorStyle, Terminal};
use std::collections::BTreeMap;
use std::collections::btree_map::Entry;
use std::io::{self, Stdout, Write};
use std::time::{Duration, Instant};
#[derive(Clone, Copy, Debug)]
pub struct RendererOptions {
pub use_alt_screen: bool,
pub hide_cursor: bool,
pub enable_mouse: bool,
pub query_capabilities: bool,
}
impl Default for RendererOptions {
fn default() -> Self {
Self {
use_alt_screen: true,
hide_cursor: true,
enable_mouse: true,
query_capabilities: true,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct RenderStats {
pub frames: u64,
pub last_frame_time: Duration,
pub last_frame_cells: usize,
pub fps: f32,
pub buffer_bytes: usize,
pub hitgrid_bytes: usize,
pub total_bytes: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Rect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl Rect {
#[must_use]
pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.width == 0 || self.height == 0
}
fn max_x(&self) -> u32 {
self.x.saturating_add(self.width)
}
fn max_y(&self) -> u32 {
self.y.saturating_add(self.height)
}
fn merge(&self, other: &Self) -> Self {
let x1 = self.x.min(other.x);
let y1 = self.y.min(other.y);
let x2 = self.max_x().max(other.max_x());
let y2 = self.max_y().max(other.max_y());
Self::new(x1, y1, x2.saturating_sub(x1), y2.saturating_sub(y1))
}
fn intersects_or_touches(&self, other: &Self) -> bool {
let x2 = self.max_x();
let y2 = self.max_y();
let ox2 = other.max_x();
let oy2 = other.max_y();
self.x <= ox2 && other.x <= x2 && self.y <= oy2 && other.y <= y2
}
fn clamp_to(&self, width: u32, height: u32) -> Option<Self> {
if self.is_empty() {
return None;
}
if self.x >= width || self.y >= height {
return None;
}
let max_x = self.max_x().min(width);
let max_y = self.max_y().min(height);
let clamped = Self::new(
self.x,
self.y,
max_x.saturating_sub(self.x),
max_y.saturating_sub(self.y),
);
if clamped.is_empty() {
None
} else {
Some(clamped)
}
}
}
pub struct Renderer {
width: u32,
height: u32,
front_buffer: OptimizedBuffer,
back_buffer: OptimizedBuffer,
terminal: Terminal<Stdout>,
front_hit_grid: HitGrid,
back_hit_grid: HitGrid,
layer_hit_grids: BTreeMap<u16, HitGrid>,
hit_scissor: ScissorStack,
link_pool: LinkPool,
grapheme_pool: crate::grapheme_pool::GraphemePool,
scratch_buffer: Vec<u8>,
cached_diff: BufferDiff,
manual_dirty_regions: Vec<Rect>,
layers: BTreeMap<u16, OptimizedBuffer>,
active_hit_layer: u16,
layers_dirty: bool,
background: Rgba,
force_redraw: bool,
stats: RenderStats,
last_present_at: Instant,
show_debug_overlay: bool,
debug_overlay_position: (u32, u32),
}
impl Renderer {
pub fn new(width: u32, height: u32) -> io::Result<Self> {
Self::new_with_options(width, height, RendererOptions::default())
}
pub fn new_with_options(width: u32, height: u32, options: RendererOptions) -> io::Result<Self> {
let mut terminal = Terminal::new(io::stdout());
if options.use_alt_screen {
terminal.enter_alt_screen()?;
}
if options.hide_cursor {
terminal.hide_cursor()?;
}
if options.enable_mouse {
terminal.enable_mouse()?;
}
if options.query_capabilities {
terminal.query_capabilities()?;
}
let total_cells = (width as usize).saturating_mul(height as usize);
Ok(Self {
width,
height,
front_buffer: OptimizedBuffer::new(width, height),
back_buffer: OptimizedBuffer::new(width, height),
terminal,
front_hit_grid: HitGrid::new(width, height),
back_hit_grid: HitGrid::new(width, height),
layer_hit_grids: BTreeMap::new(),
hit_scissor: ScissorStack::new(),
link_pool: LinkPool::new(),
grapheme_pool: crate::grapheme_pool::GraphemePool::new(),
scratch_buffer: Vec::with_capacity(total_cells.saturating_mul(20)),
cached_diff: BufferDiff::with_capacity(total_cells / 8),
manual_dirty_regions: Vec::new(),
layers: BTreeMap::new(),
active_hit_layer: 0,
layers_dirty: false,
background: Rgba::BLACK,
force_redraw: true,
stats: RenderStats::default(),
last_present_at: Instant::now(),
show_debug_overlay: false,
debug_overlay_position: (0, 0),
})
}
#[must_use]
pub fn size(&self) -> (u32, u32) {
(self.width, self.height)
}
pub fn buffer(&mut self) -> &mut OptimizedBuffer {
&mut self.back_buffer
}
pub fn buffer_with_pool(
&mut self,
) -> (
&mut OptimizedBuffer,
&mut crate::grapheme_pool::GraphemePool,
) {
(&mut self.back_buffer, &mut self.grapheme_pool)
}
#[must_use]
pub fn front_buffer(&self) -> &OptimizedBuffer {
&self.front_buffer
}
#[must_use]
pub fn stats(&self) -> &RenderStats {
&self.stats
}
pub fn set_debug_overlay(&mut self, enabled: bool) {
self.show_debug_overlay = enabled;
}
#[must_use]
pub fn is_debug_overlay_enabled(&self) -> bool {
self.show_debug_overlay
}
pub fn set_debug_overlay_position(&mut self, x: u32, y: u32) {
self.debug_overlay_position = (x, y);
}
pub fn link_pool(&mut self) -> &mut LinkPool {
&mut self.link_pool
}
pub fn grapheme_pool(&mut self) -> &mut crate::grapheme_pool::GraphemePool {
&mut self.grapheme_pool
}
#[must_use]
pub fn grapheme_pool_ref(&self) -> &crate::grapheme_pool::GraphemePool {
&self.grapheme_pool
}
#[must_use]
pub fn capabilities(&self) -> &crate::terminal::Capabilities {
self.terminal.capabilities()
}
pub fn capabilities_mut(&mut self) -> &mut crate::terminal::Capabilities {
self.terminal.capabilities_mut()
}
pub fn set_background(&mut self, color: Rgba) {
self.background = color;
}
pub fn mark_region_dirty(&mut self, rect: Rect) {
let Some(mut rect) = rect.clamp_to(self.width, self.height) else {
return;
};
let mut i = 0;
while i < self.manual_dirty_regions.len() {
if rect.intersects_or_touches(&self.manual_dirty_regions[i]) {
rect = rect.merge(&self.manual_dirty_regions[i]);
self.manual_dirty_regions.swap_remove(i);
} else {
i += 1;
}
}
self.manual_dirty_regions.push(rect);
}
#[must_use]
pub fn get_dirty_regions(&self) -> &[Rect] {
&self.manual_dirty_regions
}
pub fn render_to_layer<F>(&mut self, layer_id: u16, render_fn: F)
where
F: FnOnce(&mut OptimizedBuffer),
{
self.active_hit_layer = layer_id;
if layer_id == 0 {
render_fn(&mut self.back_buffer);
return;
}
let width = self.width;
let height = self.height;
let layer = match self.layers.entry(layer_id) {
Entry::Vacant(entry) => {
let mut buf = OptimizedBuffer::new(width, height);
buf.clear_transparent_with_pool(&mut self.grapheme_pool);
entry.insert(buf)
}
Entry::Occupied(entry) => entry.into_mut(),
};
if layer.size() != (width, height) {
layer.resize_with_pool(&mut self.grapheme_pool, width, height);
layer.clear_transparent_with_pool(&mut self.grapheme_pool);
}
render_fn(layer);
self.layers_dirty = true;
}
#[must_use]
pub fn get_layer_count(&self) -> usize {
self.layers.len()
}
pub fn merge_layers(&mut self) {
if !self.layers_dirty {
self.active_hit_layer = 0;
return;
}
for layer in self.layers.values() {
self.back_buffer
.draw_buffer_with_pool(&mut self.grapheme_pool, 0, 0, layer);
}
for grid in self.layer_hit_grids.values() {
self.back_hit_grid.overlay(grid);
}
self.layers_dirty = false;
self.active_hit_layer = 0;
}
pub fn clear(&mut self) {
self.back_buffer
.clear_with_pool(&mut self.grapheme_pool, self.background);
self.back_hit_grid.clear();
self.clear_overlay_layers();
}
pub fn present(&mut self) -> io::Result<()> {
if self.layers_dirty {
self.merge_layers();
}
if self.show_debug_overlay {
self.draw_debug_overlay();
}
let total_cells = (self.width as usize).saturating_mul(self.height as usize);
self.cached_diff
.compute_into(&self.front_buffer, &self.back_buffer);
self.append_manual_dirty_regions();
if self.force_redraw || self.cached_diff.should_full_redraw(total_cells) {
self.present_force()?;
self.update_stats(total_cells);
self.force_redraw = false;
} else {
self.present_diff()?;
self.update_stats(self.cached_diff.change_count);
}
std::mem::swap(&mut self.front_buffer, &mut self.back_buffer);
std::mem::swap(&mut self.front_hit_grid, &mut self.back_hit_grid);
self.back_buffer
.clear_with_pool(&mut self.grapheme_pool, self.background);
self.back_hit_grid.clear();
self.clear_overlay_layers();
self.manual_dirty_regions.clear();
Ok(())
}
pub fn present_force(&mut self) -> io::Result<()> {
if self.terminal.capabilities().sync_output {
self.terminal.begin_sync()?;
}
self.scratch_buffer.clear();
let mut writer = AnsiWriter::new(&mut self.scratch_buffer);
writer.write_str("\x1b[H");
for y in 0..self.height {
for x in 0..self.width {
if let Some(cell) = self.back_buffer.get(x, y) {
if !cell.is_continuation() {
writer.move_cursor(y, x);
let url = cell
.attributes
.link_id()
.and_then(|id| self.link_pool.get(id));
writer.write_cell_with_link_and_pool(cell, url, &self.grapheme_pool);
}
}
}
}
writer.reset();
writer.flush()?;
self.terminal.flush()?;
io::stdout().write_all(&self.scratch_buffer)?;
io::stdout().flush()?;
if self.terminal.capabilities().sync_output {
self.terminal.end_sync()?;
}
self.terminal.flush()
}
fn present_diff(&mut self) -> io::Result<()> {
if self.terminal.capabilities().sync_output {
self.terminal.begin_sync()?;
}
self.scratch_buffer.clear();
let mut writer = AnsiWriter::new(&mut self.scratch_buffer);
writer.write_str("\x1b[H");
for region in &self.cached_diff.dirty_regions {
if region.width == 0 || region.height == 0 {
continue;
}
for row in 0..region.height {
let y = region.y + row;
for col in 0..region.width {
let x = region.x + col;
let back_cell = self.back_buffer.get(x, y);
if let Some(cell) = back_cell {
if cell.is_continuation() {
continue;
}
writer.move_cursor(y, x);
let url = cell
.attributes
.link_id()
.and_then(|id| self.link_pool.get(id));
writer.write_cell_with_pool_and_link(cell, &self.grapheme_pool, url);
}
}
}
}
writer.reset();
writer.flush()?;
if !self.scratch_buffer.is_empty() {
io::stdout().write_all(&self.scratch_buffer)?;
io::stdout().flush()?;
}
if self.terminal.capabilities().sync_output {
self.terminal.end_sync()?;
}
self.terminal.flush()
}
pub fn resize(&mut self, width: u32, height: u32) -> io::Result<()> {
self.width = width;
self.height = height;
self.front_buffer
.resize_with_pool(&mut self.grapheme_pool, width, height);
self.back_buffer
.resize_with_pool(&mut self.grapheme_pool, width, height);
self.front_hit_grid = HitGrid::new(width, height);
self.back_hit_grid = HitGrid::new(width, height);
self.resize_overlay_layers(width, height);
self.hit_scissor.clear();
self.cached_diff.clear();
self.manual_dirty_regions.clear();
self.force_redraw = true;
self.terminal.clear()
}
pub fn set_cursor(&mut self, x: u32, y: u32, visible: bool) -> io::Result<()> {
if visible {
self.terminal.show_cursor()?;
self.terminal.move_cursor(x, y)?;
} else {
self.terminal.hide_cursor()?;
}
Ok(())
}
pub fn set_cursor_style(&mut self, style: CursorStyle, blinking: bool) -> io::Result<()> {
self.terminal.set_cursor_style(style, blinking)
}
pub fn set_title(&mut self, title: &str) -> io::Result<()> {
self.terminal.set_title(title)
}
pub fn register_hit_area(&mut self, x: u32, y: u32, width: u32, height: u32, id: u32) {
let rect = ClipRect::new(x as i32, y as i32, width, height);
if let Some(intersect) = self.hit_scissor.current().intersect(&rect) {
if !intersect.is_empty() {
let hit_grid = if self.active_hit_layer == 0 {
&mut self.back_hit_grid
} else {
let width = self.width;
let height = self.height;
let grid = self
.layer_hit_grids
.entry(self.active_hit_layer)
.or_insert_with(|| HitGrid::new(width, height));
if grid.size() != (width, height) {
grid.resize(width, height);
}
self.layers_dirty = true;
grid
};
hit_grid.register(
intersect.x.max(0) as u32,
intersect.y.max(0) as u32,
intersect.width,
intersect.height,
id,
);
}
}
}
#[must_use]
pub fn hit_test(&self, x: u32, y: u32) -> Option<u32> {
self.front_hit_grid.test(x, y)
}
pub fn push_hit_scissor(&mut self, rect: ClipRect) {
self.hit_scissor.push(rect);
}
pub fn pop_hit_scissor(&mut self) {
self.hit_scissor.pop();
}
pub fn clear_hit_scissors(&mut self) {
self.hit_scissor.clear();
}
pub fn invalidate(&mut self) {
self.force_redraw = true;
}
pub fn cleanup(&mut self) -> io::Result<()> {
self.terminal.cleanup()
}
fn update_stats(&mut self, cells_updated: usize) {
let now = Instant::now();
let frame_time = now.duration_since(self.last_present_at);
self.last_present_at = now;
self.stats.frames = self.stats.frames.saturating_add(1);
self.stats.last_frame_time = frame_time;
self.stats.last_frame_cells = cells_updated;
self.stats.fps = if frame_time.as_secs_f32() > 0.0 {
1.0 / frame_time.as_secs_f32()
} else {
0.0
};
let buffer_bytes = self.front_buffer.byte_size() + self.back_buffer.byte_size();
let hitgrid_bytes = self.front_hit_grid.byte_size() + self.back_hit_grid.byte_size();
self.stats.buffer_bytes = buffer_bytes;
self.stats.hitgrid_bytes = hitgrid_bytes;
self.stats.total_bytes = buffer_bytes + hitgrid_bytes;
}
fn draw_debug_overlay(&mut self) {
let stats = &self.stats;
let total_cells = (self.width as usize).saturating_mul(self.height as usize);
let dirty_regions = self.cached_diff.dirty_regions.len();
let layers = self.layers.len();
let pool_stats = self.grapheme_pool.stats();
let pool_mem = self.grapheme_pool.get_memory_usage();
let mem_total = stats.total_bytes.saturating_add(pool_mem);
let lines = [
format!(
"FPS: {:.1} ({:.1}ms)",
stats.fps,
stats.last_frame_time.as_secs_f32() * 1000.0
),
format!("Cells: {} / {}", stats.last_frame_cells, total_cells),
format!("Dirty: {dirty_regions} regions"),
format!("Layers: {layers}"),
format!(
"Pool: {}/{} active",
pool_stats.active_slots, pool_stats.total_slots
),
format!("Mem: {mem_total} B"),
];
let title = "Debug";
let max_line_width = lines
.iter()
.map(|line| crate::unicode::display_width(line))
.max()
.unwrap_or(0);
let min_width = crate::unicode::display_width(title).saturating_add(4);
let inner_width = max_line_width.max(min_width);
let box_w = (inner_width + 2) as u32;
let box_h = (lines.len() + 2) as u32;
if box_w < 2 || box_h < 2 || self.width < 2 || self.height < 2 {
return;
}
let (mut x, mut y) = self.debug_overlay_position;
let max_x = self.width.saturating_sub(box_w);
let max_y = self.height.saturating_sub(box_h);
x = x.min(max_x);
y = y.min(max_y);
let border_style = crate::style::Style::fg(Rgba::WHITE).with_bold();
let mut options = BoxOptions::new(BoxStyle::rounded(border_style));
options.fill = Some(Rgba::BLACK.with_alpha(0.6));
options.title = Some(title.to_string());
options.title_align = TitleAlign::Left;
self.back_buffer
.draw_box_with_options(x, y, box_w, box_h, options);
let text_style = crate::style::Style::fg(Rgba::WHITE);
for (idx, line) in lines.iter().enumerate() {
let row = y.saturating_add(1 + idx as u32);
self.back_buffer
.draw_text(x.saturating_add(1), row, line, text_style);
}
}
fn clear_overlay_layers(&mut self) {
for layer in self.layers.values_mut() {
layer.clear_transparent_with_pool(&mut self.grapheme_pool);
}
for grid in self.layer_hit_grids.values_mut() {
grid.clear();
}
self.active_hit_layer = 0;
self.layers_dirty = false;
}
fn resize_overlay_layers(&mut self, width: u32, height: u32) {
for layer in self.layers.values_mut() {
layer.resize_with_pool(&mut self.grapheme_pool, width, height);
layer.clear_transparent_with_pool(&mut self.grapheme_pool);
}
for grid in self.layer_hit_grids.values_mut() {
grid.resize(width, height);
}
self.active_hit_layer = 0;
self.layers_dirty = false;
}
fn append_manual_dirty_regions(&mut self) {
if self.manual_dirty_regions.is_empty() {
return;
}
self.cached_diff
.dirty_regions
.reserve(self.manual_dirty_regions.len());
for rect in &self.manual_dirty_regions {
self.cached_diff.dirty_regions.push(diff::DirtyRegion::new(
rect.x,
rect.y,
rect.width,
rect.height,
));
}
}
}
impl Drop for Renderer {
fn drop(&mut self) {
let _ = self.cleanup();
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::float_cmp)] use super::*;
use crate::cell::Cell;
#[test]
fn test_renderer_options_default() {
let opts = RendererOptions::default();
assert!(opts.use_alt_screen);
assert!(opts.hide_cursor);
assert!(opts.enable_mouse);
assert!(opts.query_capabilities);
}
#[test]
fn test_renderer_options_custom() {
let opts = RendererOptions {
use_alt_screen: false,
hide_cursor: false,
enable_mouse: false,
query_capabilities: false,
};
assert!(!opts.use_alt_screen);
assert!(!opts.hide_cursor);
assert!(!opts.enable_mouse);
assert!(!opts.query_capabilities);
}
#[test]
fn test_renderer_options_copy() {
let opts = RendererOptions::default();
let copy = opts;
assert_eq!(opts.use_alt_screen, copy.use_alt_screen);
}
#[test]
fn test_render_stats_default() {
let stats = RenderStats::default();
assert_eq!(stats.frames, 0);
assert_eq!(stats.last_frame_cells, 0);
assert_eq!(stats.fps, 0.0);
assert_eq!(stats.buffer_bytes, 0);
assert_eq!(stats.hitgrid_bytes, 0);
assert_eq!(stats.total_bytes, 0);
}
#[test]
fn test_render_stats_clone() {
let stats = RenderStats {
frames: 100,
last_frame_time: Duration::from_millis(16),
last_frame_cells: 1920,
fps: 60.0,
buffer_bytes: 10000,
hitgrid_bytes: 5000,
total_bytes: 15000,
};
let cloned = stats.clone();
assert_eq!(cloned.frames, 100);
assert_eq!(cloned.fps, 60.0);
}
#[test]
fn test_buffer_access() {
let front = OptimizedBuffer::new(80, 24);
let back = OptimizedBuffer::new(80, 24);
assert_eq!(front.size(), back.size());
}
#[test]
fn test_buffer_composition_double_buffer() {
let mut front = OptimizedBuffer::new(80, 24);
let mut back = OptimizedBuffer::new(80, 24);
back.set(10, 5, Cell::new('X', crate::style::Style::NONE));
std::mem::swap(&mut front, &mut back);
assert!(front.get(10, 5).is_some());
let cell = front.get(10, 5).unwrap();
assert!(matches!(cell.content, crate::cell::CellContent::Char('X')));
}
#[test]
fn test_buffer_composition_resize() {
let mut buf = OptimizedBuffer::new(80, 24);
assert_eq!(buf.size(), (80, 24));
let mut pool = crate::grapheme_pool::GraphemePool::new();
buf.resize_with_pool(&mut pool, 100, 50);
assert_eq!(buf.size(), (100, 50));
}
#[test]
fn test_buffer_composition_clear() {
let mut buf = OptimizedBuffer::new(80, 24);
buf.set(0, 0, Cell::clear(Rgba::RED));
let mut pool = crate::grapheme_pool::GraphemePool::new();
buf.clear_with_pool(&mut pool, Rgba::BLACK);
let cell = buf.get(0, 0).unwrap();
assert_eq!(cell.bg, Rgba::BLACK);
}
#[test]
fn test_buffer_diff_integration() {
let front = OptimizedBuffer::new(80, 24);
let mut back = OptimizedBuffer::new(80, 24);
back.set(40, 12, Cell::new('A', crate::style::Style::fg(Rgba::RED)));
let diff = BufferDiff::compute(&front, &back);
assert!(!diff.is_empty());
assert!(diff.changed_cells.contains(&(40, 12)));
}
#[test]
fn test_buffer_diff_no_changes() {
let front = OptimizedBuffer::new(80, 24);
let back = OptimizedBuffer::new(80, 24);
let diff = BufferDiff::compute(&front, &back);
assert!(diff.is_empty());
}
#[test]
fn test_buffer_diff_full_redraw_threshold() {
let front = OptimizedBuffer::new(10, 10);
let mut back = OptimizedBuffer::new(10, 10);
for y in 0..10 {
for x in 0..6 {
back.set(x, y, Cell::clear(Rgba::RED));
}
}
let diff = BufferDiff::compute(&front, &back);
let total_cells = 100;
assert!(diff.should_full_redraw(total_cells));
}
#[test]
fn test_hit_grid_integration() {
let mut grid = HitGrid::new(80, 24);
grid.register(10, 5, 20, 3, 1);
assert_eq!(grid.hit_test(15, 6), Some(1));
assert_eq!(grid.hit_test(5, 6), None);
}
#[test]
fn test_hit_grid_clear_integration() {
let mut grid = HitGrid::new(80, 24);
grid.register(0, 0, 80, 24, 1);
assert_eq!(grid.hit_test(40, 12), Some(1));
grid.clear();
assert_eq!(grid.hit_test(40, 12), None);
}
#[test]
fn test_scissor_stack_hit_clipping() {
let mut scissor = ScissorStack::new();
let full = scissor.current();
assert!(full.contains(0, 0));
scissor.push(ClipRect::new(10, 10, 20, 20));
let clipped = scissor.current();
assert!(!clipped.contains(5, 5));
assert!(clipped.contains(15, 15));
scissor.pop();
let restored = scissor.current();
assert!(restored.contains(5, 5));
}
#[test]
fn test_link_pool_allocation() {
let mut pool = LinkPool::new();
let id1 = pool.alloc("https://example.com");
let id2 = pool.alloc("https://other.com");
assert!(id1 != id2);
assert_eq!(pool.get(id1), Some("https://example.com"));
assert_eq!(pool.get(id2), Some("https://other.com"));
}
#[test]
fn test_link_pool_refcounting() {
let mut pool = LinkPool::new();
let id = pool.alloc("https://example.com");
pool.incref(id);
pool.decref(id);
assert!(pool.get(id).is_some());
pool.decref(id);
assert!(pool.get(id).is_none());
}
#[test]
fn test_grapheme_pool_allocation() {
let mut pool = crate::grapheme_pool::GraphemePool::new();
let id = pool.alloc("👨👩👧");
assert!(pool.get(id).is_some());
assert_eq!(pool.get(id), Some("👨👩👧"));
}
#[test]
fn test_zero_size_buffer() {
let buf = OptimizedBuffer::new(0, 0);
assert_eq!(buf.size(), (1, 1));
}
#[test]
fn test_single_cell_buffer() {
let mut buf = OptimizedBuffer::new(1, 1);
buf.set(0, 0, Cell::new('X', crate::style::Style::NONE));
assert!(buf.get(0, 0).is_some());
}
#[test]
fn test_large_buffer() {
let buf = OptimizedBuffer::new(500, 200);
assert_eq!(buf.size(), (500, 200));
}
#[test]
fn test_buffer_out_of_bounds() {
let buf = OptimizedBuffer::new(80, 24);
assert!(buf.get(80, 24).is_none());
assert!(buf.get(100, 100).is_none());
}
#[test]
fn test_dirty_region_creation() {
let region = diff::DirtyRegion::new(10, 20, 30, 40);
assert_eq!(region.x, 10);
assert_eq!(region.y, 20);
assert_eq!(region.width, 30);
assert_eq!(region.height, 40);
}
#[test]
fn test_dirty_region_cell() {
let region = diff::DirtyRegion::cell(5, 10);
assert_eq!(region.x, 5);
assert_eq!(region.y, 10);
assert_eq!(region.width, 1);
assert_eq!(region.height, 1);
}
#[test]
fn test_mark_region_dirty_clamps_and_stores() {
let mut r = test_renderer(10, 10);
r.mark_region_dirty(Rect::new(2, 3, 4, 5));
let regions = r.get_dirty_regions();
assert_eq!(regions.len(), 1);
assert!(regions.contains(&Rect::new(2, 3, 4, 5)));
r.mark_region_dirty(Rect::new(8, 8, 10, 10));
let regions = r.get_dirty_regions();
assert_eq!(regions.len(), 2);
assert!(regions.contains(&Rect::new(8, 8, 2, 2)));
}
#[test]
fn test_mark_region_dirty_merges_overlaps() {
let mut r = test_renderer(20, 20);
r.mark_region_dirty(Rect::new(2, 2, 4, 4));
r.mark_region_dirty(Rect::new(4, 4, 4, 4));
let regions = r.get_dirty_regions();
assert_eq!(regions.len(), 1);
assert_eq!(regions[0], Rect::new(2, 2, 6, 6));
}
#[test]
fn test_dirty_regions_cleared_after_present() {
let mut r = test_renderer(5, 5);
r.mark_region_dirty(Rect::new(1, 1, 2, 2));
assert!(!r.get_dirty_regions().is_empty());
r.present().unwrap();
assert!(r.get_dirty_regions().is_empty());
}
#[test]
fn test_buffer_preserves_content_on_same_set() {
let mut buf = OptimizedBuffer::new(10, 10);
let cell = Cell::new('A', crate::style::Style::fg(Rgba::RED));
buf.set(5, 5, cell);
buf.set(5, 5, cell);
let stored = buf.get(5, 5).unwrap();
assert!(matches!(
stored.content,
crate::cell::CellContent::Char('A')
));
}
#[test]
fn test_buffer_multiple_sets_same_cell() {
let mut buf = OptimizedBuffer::new(10, 10);
buf.set(5, 5, Cell::new('A', crate::style::Style::NONE));
buf.set(5, 5, Cell::new('B', crate::style::Style::NONE));
buf.set(5, 5, Cell::new('C', crate::style::Style::NONE));
let stored = buf.get(5, 5).unwrap();
assert!(matches!(
stored.content,
crate::cell::CellContent::Char('C')
));
}
#[test]
fn test_stats_byte_size_calculation() {
let buf = OptimizedBuffer::new(80, 24);
let byte_size = buf.byte_size();
assert!(byte_size > 0);
}
#[test]
fn test_hit_grid_byte_size() {
let grid = HitGrid::new(80, 24);
let byte_size = grid.byte_size();
let expected = 80 * 24 * std::mem::size_of::<Option<u32>>();
assert_eq!(byte_size, expected);
}
impl HitGrid {
fn hit_test(&self, x: u32, y: u32) -> Option<u32> {
self.test(x, y)
}
}
fn test_renderer(width: u32, height: u32) -> Renderer {
Renderer::new_with_options(
width,
height,
RendererOptions {
use_alt_screen: false,
hide_cursor: false,
enable_mouse: false,
query_capabilities: false,
},
)
.expect("test renderer creation should succeed with disabled options")
}
fn commit_hits_for_test(r: &mut Renderer) {
if r.layers_dirty {
r.merge_layers();
}
std::mem::swap(&mut r.front_hit_grid, &mut r.back_hit_grid);
r.back_hit_grid.clear();
}
#[test]
fn test_renderer_new_with_all_options_disabled() {
let result = Renderer::new_with_options(
80,
24,
RendererOptions {
use_alt_screen: false,
hide_cursor: false,
enable_mouse: false,
query_capabilities: false,
},
);
assert!(result.is_ok());
}
#[test]
fn test_renderer_new_stores_dimensions() {
let r = test_renderer(80, 24);
assert_eq!(r.size(), (80, 24));
}
#[test]
fn test_renderer_new_creates_matching_back_buffer() {
let mut r = test_renderer(80, 24);
assert_eq!(r.buffer().size(), (80, 24));
}
#[test]
fn test_renderer_new_creates_matching_front_buffer() {
let r = test_renderer(80, 24);
assert_eq!(r.front_buffer().size(), (80, 24));
}
#[test]
fn test_renderer_new_various_dimensions() {
for &(w, h) in &[(1, 1), (10, 10), (200, 50), (1, 100), (100, 1)] {
let r = test_renderer(w, h);
assert_eq!(r.size(), (w, h), "Failed for dimensions ({w}, {h})");
}
}
#[test]
fn test_renderer_new_initializes_zero_stats() {
let r = test_renderer(80, 24);
let stats = r.stats();
assert_eq!(stats.frames, 0);
assert_eq!(stats.last_frame_cells, 0);
assert_eq!(stats.fps, 0.0);
}
#[test]
fn test_renderer_new_default_background_black() {
let mut r = test_renderer(10, 10);
r.clear();
let cell = r.buffer().get(0, 0).unwrap();
assert_eq!(cell.bg, Rgba::BLACK);
}
#[test]
fn test_renderer_new_single_cell() {
let r = test_renderer(1, 1);
assert_eq!(r.size(), (1, 1));
assert!(r.front_buffer().get(0, 0).is_some());
}
#[test]
fn test_resize_changes_reported_dimensions() {
let mut r = test_renderer(80, 24);
r.resize(100, 50).unwrap();
assert_eq!(r.size(), (100, 50));
}
#[test]
fn test_resize_changes_back_buffer_size() {
let mut r = test_renderer(80, 24);
r.resize(100, 50).unwrap();
assert_eq!(r.buffer().size(), (100, 50));
}
#[test]
fn test_resize_changes_front_buffer_size() {
let mut r = test_renderer(80, 24);
r.resize(100, 50).unwrap();
assert_eq!(r.front_buffer().size(), (100, 50));
}
#[test]
fn test_resize_shrink() {
let mut r = test_renderer(80, 24);
r.resize(40, 12).unwrap();
assert_eq!(r.size(), (40, 12));
assert_eq!(r.buffer().size(), (40, 12));
}
#[test]
fn test_resize_grow() {
let mut r = test_renderer(40, 12);
r.resize(160, 48).unwrap();
assert_eq!(r.size(), (160, 48));
assert_eq!(r.buffer().size(), (160, 48));
}
#[test]
fn test_resize_to_single_cell() {
let mut r = test_renderer(80, 24);
r.resize(1, 1).unwrap();
assert_eq!(r.size(), (1, 1));
}
#[test]
fn test_resize_sets_force_redraw() {
let mut r = test_renderer(10, 10);
r.present().unwrap();
r.resize(20, 20).unwrap();
r.present().unwrap();
assert_eq!(r.stats().last_frame_cells, 400);
}
#[test]
fn test_resize_clears_hit_grid() {
let mut r = test_renderer(80, 24);
r.register_hit_area(10, 5, 20, 3, 42);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(15, 6), Some(42));
r.resize(80, 24).unwrap();
assert_eq!(r.hit_test(15, 6), None);
}
#[test]
fn test_invalidate_does_not_panic() {
let mut r = test_renderer(80, 24);
r.invalidate();
}
#[test]
fn test_invalidate_multiple_times() {
let mut r = test_renderer(80, 24);
r.invalidate();
r.invalidate();
r.invalidate();
}
#[test]
fn test_invalidate_forces_full_redraw() {
let mut r = test_renderer(10, 10);
r.present().unwrap();
assert_eq!(r.stats().last_frame_cells, 100);
r.invalidate();
r.present().unwrap();
assert_eq!(r.stats().last_frame_cells, 100);
}
#[test]
fn test_cleanup_succeeds() {
let mut r = test_renderer(80, 24);
assert!(r.cleanup().is_ok());
}
#[test]
fn test_cleanup_idempotent() {
let mut r = test_renderer(80, 24);
r.cleanup().unwrap();
r.cleanup().unwrap();
}
#[test]
fn test_cleanup_on_fresh_renderer() {
let mut r = test_renderer(80, 24);
assert!(r.cleanup().is_ok());
}
#[test]
fn test_renderer_buffer_is_writable() {
let mut r = test_renderer(10, 10);
r.buffer()
.set(5, 5, Cell::new('X', crate::style::Style::NONE));
let cell = r.buffer().get(5, 5).unwrap();
assert!(matches!(cell.content, crate::cell::CellContent::Char('X')));
}
#[test]
fn test_renderer_buffer_with_pool() {
let mut r = test_renderer(10, 10);
let (buf, pool) = r.buffer_with_pool();
let id = pool.alloc("👨👩👧");
assert!(pool.get(id).is_some());
assert_eq!(buf.size(), (10, 10));
}
#[test]
fn test_renderer_front_buffer_readable() {
let r = test_renderer(10, 10);
let front = r.front_buffer();
assert!(front.get(0, 0).is_some());
assert_eq!(front.size(), (10, 10));
}
#[test]
fn test_set_background_affects_clear() {
let mut r = test_renderer(10, 10);
r.set_background(Rgba::RED);
r.clear();
let cell = r.buffer().get(0, 0).unwrap();
assert_eq!(cell.bg, Rgba::RED);
}
#[test]
fn test_set_background_multiple_colors() {
let mut r = test_renderer(10, 10);
for color in [Rgba::RED, Rgba::GREEN, Rgba::BLUE, Rgba::WHITE] {
r.set_background(color);
r.clear();
let cell = r.buffer().get(0, 0).unwrap();
assert_eq!(cell.bg, color);
}
}
#[test]
fn test_set_debug_overlay_toggle() {
let mut r = test_renderer(80, 24);
assert!(!r.is_debug_overlay_enabled());
r.set_debug_overlay(true);
assert!(r.is_debug_overlay_enabled());
r.set_debug_overlay(false);
assert!(!r.is_debug_overlay_enabled());
r.set_debug_overlay(true);
assert!(r.is_debug_overlay_enabled());
}
#[test]
fn test_debug_overlay_position_draws_box() {
let mut r = test_renderer(40, 20);
r.set_debug_overlay(true);
r.set_debug_overlay_position(2, 3);
r.draw_debug_overlay();
let cell = r.buffer().get(2, 3).unwrap();
assert!(matches!(cell.content, crate::cell::CellContent::Char('╭')));
}
#[test]
fn test_renderer_capabilities_accessible() {
let r = test_renderer(80, 24);
let caps = r.capabilities();
let _ = caps.hyperlinks;
let _ = caps.sync_output;
}
#[test]
fn test_renderer_capabilities_mut_writable() {
let mut r = test_renderer(80, 24);
r.capabilities_mut().hyperlinks = true;
assert!(r.capabilities().hyperlinks);
r.capabilities_mut().hyperlinks = false;
assert!(!r.capabilities().hyperlinks);
}
#[test]
fn test_renderer_link_pool_usable() {
let mut r = test_renderer(80, 24);
let id = r.link_pool().alloc("https://example.com");
assert_eq!(r.link_pool().get(id), Some("https://example.com"));
}
#[test]
fn test_renderer_grapheme_pool_usable() {
let mut r = test_renderer(80, 24);
let id = r.grapheme_pool().alloc("test_grapheme");
assert_eq!(r.grapheme_pool().get(id), Some("test_grapheme"));
}
#[test]
fn test_renderer_grapheme_pool_ref_readable() {
let r = test_renderer(80, 24);
let _pool = r.grapheme_pool_ref();
}
#[test]
fn test_renderer_register_and_hit_test() {
let mut r = test_renderer(80, 24);
r.register_hit_area(10, 5, 20, 3, 42);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(15, 6), Some(42));
assert_eq!(r.hit_test(5, 3), None);
}
#[test]
fn test_renderer_hit_scissor_clips_registration() {
let mut r = test_renderer(80, 24);
r.push_hit_scissor(ClipRect::new(10, 10, 20, 20));
r.register_hit_area(0, 0, 80, 24, 1);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(15, 15), Some(1));
assert_eq!(r.hit_test(5, 5), None);
r.pop_hit_scissor();
}
#[test]
fn test_renderer_clear_hit_scissors_restores_full_area() {
let mut r = test_renderer(80, 24);
r.push_hit_scissor(ClipRect::new(10, 10, 5, 5));
r.clear_hit_scissors();
r.register_hit_area(0, 0, 80, 24, 1);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(5, 5), Some(1));
}
#[test]
fn test_present_succeeds_on_fresh_renderer() {
let mut r = test_renderer(80, 24);
assert!(r.present().is_ok());
}
#[test]
fn test_present_increments_frame_count() {
let mut r = test_renderer(10, 10);
r.present().unwrap();
assert_eq!(r.stats().frames, 1);
r.present().unwrap();
assert_eq!(r.stats().frames, 2);
r.present().unwrap();
assert_eq!(r.stats().frames, 3);
}
#[test]
fn test_present_updates_stats() {
let mut r = test_renderer(10, 10);
r.buffer()
.set(0, 0, Cell::new('X', crate::style::Style::NONE));
r.present().unwrap();
assert!(r.stats().frames > 0);
assert!(r.stats().buffer_bytes > 0);
assert!(r.stats().total_bytes > 0);
}
#[test]
fn test_present_force_succeeds() {
let mut r = test_renderer(10, 10);
assert!(r.present_force().is_ok());
}
#[test]
fn test_present_swaps_buffers() {
let mut r = test_renderer(10, 10);
r.buffer()
.set(5, 5, Cell::new('A', crate::style::Style::NONE));
let front_cell = r.front_buffer().get(5, 5).unwrap();
assert!(!matches!(
front_cell.content,
crate::cell::CellContent::Char('A')
));
r.present().unwrap();
let front_cell = r.front_buffer().get(5, 5).unwrap();
assert!(matches!(
front_cell.content,
crate::cell::CellContent::Char('A')
));
}
#[test]
fn test_present_clears_back_buffer_after_swap() {
let mut r = test_renderer(10, 10);
r.buffer()
.set(5, 5, Cell::new('Z', crate::style::Style::NONE));
r.present().unwrap();
let back_cell = r.buffer().get(5, 5).unwrap();
assert!(
!matches!(back_cell.content, crate::cell::CellContent::Char('Z')),
"Back buffer should be cleared after present"
);
}
#[test]
fn test_renderer_clear_resets_buffer() {
let mut r = test_renderer(10, 10);
r.buffer()
.set(0, 0, Cell::new('X', crate::style::Style::NONE));
r.clear();
let cell = r.buffer().get(0, 0).unwrap();
assert_eq!(cell.bg, Rgba::BLACK);
}
#[test]
fn test_renderer_clear_resets_hit_grid() {
let mut r = test_renderer(80, 24);
r.register_hit_area(0, 0, 80, 24, 1);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(5, 5), Some(1));
r.clear();
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(5, 5), None);
}
#[test]
fn test_renderer_multiple_non_overlapping_hit_areas() {
let mut r = test_renderer(80, 24);
r.register_hit_area(0, 0, 10, 5, 1);
r.register_hit_area(20, 0, 10, 5, 2);
r.register_hit_area(40, 0, 10, 5, 3);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(5, 2), Some(1));
assert_eq!(r.hit_test(25, 2), Some(2));
assert_eq!(r.hit_test(45, 2), Some(3));
assert_eq!(r.hit_test(15, 2), None); }
#[test]
fn test_renderer_overlapping_hit_areas_later_wins() {
let mut r = test_renderer(80, 24);
r.register_hit_area(0, 0, 30, 10, 100);
r.register_hit_area(20, 0, 30, 10, 200);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(5, 5), Some(100)); assert_eq!(r.hit_test(25, 5), Some(200)); assert_eq!(r.hit_test(45, 5), Some(200)); }
#[test]
fn test_renderer_hit_test_boundary_conditions() {
let mut r = test_renderer(80, 24);
r.register_hit_area(10, 5, 20, 10, 1);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(10, 5), Some(1)); assert_eq!(r.hit_test(29, 5), Some(1)); assert_eq!(r.hit_test(10, 14), Some(1)); assert_eq!(r.hit_test(29, 14), Some(1));
assert_eq!(r.hit_test(9, 5), None);
assert_eq!(r.hit_test(30, 5), None);
assert_eq!(r.hit_test(10, 4), None);
assert_eq!(r.hit_test(10, 15), None);
}
#[test]
fn test_renderer_hit_area_outside_buffer_bounds() {
let mut r = test_renderer(20, 20);
r.register_hit_area(15, 15, 20, 20, 1);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(18, 18), Some(1));
assert_eq!(r.hit_test(25, 25), None);
}
#[test]
fn test_renderer_nested_hit_scissors() {
let mut r = test_renderer(80, 24);
r.push_hit_scissor(ClipRect::new(5, 5, 40, 20));
r.push_hit_scissor(ClipRect::new(10, 8, 20, 10));
r.register_hit_area(0, 0, 80, 24, 1);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(15, 12), Some(1)); assert_eq!(r.hit_test(7, 7), None); assert_eq!(r.hit_test(2, 2), None);
r.pop_hit_scissor(); r.pop_hit_scissor(); }
#[test]
fn test_renderer_pop_hit_scissor_restores_previous() {
let mut r = test_renderer(80, 24);
r.push_hit_scissor(ClipRect::new(10, 10, 10, 10));
r.register_hit_area(0, 0, 80, 24, 1);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(15, 15), Some(1));
assert_eq!(r.hit_test(5, 5), None);
r.pop_hit_scissor();
r.register_hit_area(0, 0, 80, 24, 2);
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(5, 5), Some(2));
}
#[test]
fn test_renderer_hit_test_after_present() {
let mut r = test_renderer(80, 24);
r.register_hit_area(10, 5, 20, 3, 42);
r.buffer()
.draw_text(0, 0, "Hello", crate::style::Style::NONE);
r.present().unwrap();
assert_eq!(r.hit_test(15, 6), Some(42));
r.present().unwrap();
assert_eq!(r.hit_test(15, 6), None);
}
#[test]
fn test_renderer_hit_test_none_on_empty() {
let r = test_renderer(80, 24);
assert_eq!(r.hit_test(0, 0), None);
assert_eq!(r.hit_test(40, 12), None);
assert_eq!(r.hit_test(79, 23), None);
}
#[test]
fn test_get_layer_count_tracks_allocated_layers() {
let mut r = test_renderer(10, 10);
assert_eq!(r.get_layer_count(), 0);
r.render_to_layer(1, |_| {});
assert_eq!(r.get_layer_count(), 1);
r.render_to_layer(5, |_| {});
assert_eq!(r.get_layer_count(), 2);
r.render_to_layer(1, |_| {});
assert_eq!(r.get_layer_count(), 2);
}
#[test]
fn test_merge_layers_composites_higher_layers_on_top() {
let mut r = test_renderer(3, 1);
r.buffer()
.set(0, 0, Cell::new('A', crate::style::Style::NONE));
r.buffer()
.set(1, 0, Cell::new('X', crate::style::Style::NONE));
r.render_to_layer(1, |buf| {
buf.set(0, 0, Cell::new('B', crate::style::Style::NONE));
});
r.render_to_layer(2, |buf| {
buf.set(0, 0, Cell::new('C', crate::style::Style::NONE));
});
r.merge_layers();
let top = r.buffer().get(0, 0).unwrap();
assert!(matches!(top.content, crate::cell::CellContent::Char('C')));
let base_only = r.buffer().get(1, 0).unwrap();
assert!(matches!(
base_only.content,
crate::cell::CellContent::Char('X')
));
}
#[test]
fn test_merge_layers_does_not_tint_base_fg_when_layer_is_transparent() {
let mut r = test_renderer(1, 1);
r.buffer().set(
0,
0,
Cell::new(
'A',
crate::style::Style::builder()
.fg(Rgba::RED)
.bg(Rgba::BLACK)
.build(),
),
);
r.render_to_layer(1, |_| {});
r.merge_layers();
let cell = r.buffer().get(0, 0).unwrap();
assert_eq!(cell.fg, Rgba::RED);
assert_eq!(cell.bg, Rgba::BLACK);
}
#[test]
fn test_merge_layers_composites_hit_grids_by_layer_id() {
let mut r = test_renderer(10, 10);
r.render_to_layer(1, |_| {});
r.register_hit_area(0, 0, 1, 1, 111);
r.render_to_layer(0, |_| {});
r.register_hit_area(0, 0, 1, 1, 222);
r.register_hit_area(1, 0, 1, 1, 333);
r.merge_layers();
commit_hits_for_test(&mut r);
assert_eq!(r.hit_test(0, 0), Some(111));
assert_eq!(r.hit_test(1, 0), Some(333));
}
#[test]
fn test_present_merges_layers_automatically() {
let mut r = test_renderer(2, 1);
r.buffer()
.set(0, 0, Cell::new('A', crate::style::Style::NONE));
r.render_to_layer(1, |buf| {
buf.set(0, 0, Cell::new('B', crate::style::Style::NONE));
});
r.present().unwrap();
let cell = r.front_buffer().get(0, 0).unwrap();
assert!(matches!(cell.content, crate::cell::CellContent::Char('B')));
}
#[test]
fn test_set_cursor_visible() {
let mut r = test_renderer(80, 24);
assert!(r.set_cursor(10, 5, true).is_ok());
}
#[test]
fn test_set_cursor_hidden() {
let mut r = test_renderer(80, 24);
assert!(r.set_cursor(0, 0, false).is_ok());
}
#[test]
fn test_set_cursor_at_origin() {
let mut r = test_renderer(80, 24);
assert!(r.set_cursor(0, 0, true).is_ok());
}
#[test]
fn test_set_cursor_at_boundary() {
let mut r = test_renderer(80, 24);
assert!(r.set_cursor(79, 23, true).is_ok());
}
#[test]
fn test_set_cursor_style_block() {
let mut r = test_renderer(80, 24);
assert!(r.set_cursor_style(CursorStyle::Block, false).is_ok());
assert!(r.set_cursor_style(CursorStyle::Block, true).is_ok());
}
#[test]
fn test_set_cursor_style_underline() {
let mut r = test_renderer(80, 24);
assert!(r.set_cursor_style(CursorStyle::Underline, false).is_ok());
assert!(r.set_cursor_style(CursorStyle::Underline, true).is_ok());
}
#[test]
fn test_set_cursor_style_bar() {
let mut r = test_renderer(80, 24);
assert!(r.set_cursor_style(CursorStyle::Bar, false).is_ok());
assert!(r.set_cursor_style(CursorStyle::Bar, true).is_ok());
}
#[test]
fn test_set_title_basic() {
let mut r = test_renderer(80, 24);
assert!(r.set_title("OpenTUI").is_ok());
}
#[test]
fn test_set_title_empty() {
let mut r = test_renderer(80, 24);
assert!(r.set_title("").is_ok());
}
#[test]
fn test_set_title_special_characters() {
let mut r = test_renderer(80, 24);
assert!(r.set_title("Hello — World 🌍").is_ok());
}
#[test]
fn test_set_title_with_unicode() {
let mut r = test_renderer(80, 24);
assert!(r.set_title("日本語タイトル").is_ok());
}
#[test]
fn test_capabilities_color_support_readable() {
let r = test_renderer(80, 24);
let caps = r.capabilities();
let _ = caps.color;
let _ = caps.unicode;
let _ = caps.mouse;
}
#[test]
fn test_capabilities_override_persists() {
let mut r = test_renderer(80, 24);
r.capabilities_mut().sync_output = true;
assert!(r.capabilities().sync_output);
r.capabilities_mut().sync_output = false;
assert!(!r.capabilities().sync_output);
}
#[test]
fn test_capabilities_hyperlinks_override() {
let mut r = test_renderer(80, 24);
let original = r.capabilities().hyperlinks;
r.capabilities_mut().hyperlinks = !original;
assert_eq!(r.capabilities().hyperlinks, !original);
}
#[test]
fn test_set_background_and_present() {
let mut r = test_renderer(10, 10);
r.set_background(Rgba::BLUE);
r.clear();
let cell = r.buffer().get(5, 5).unwrap();
assert_eq!(cell.bg, Rgba::BLUE);
assert!(r.present().is_ok());
}
}