ref:main
//! Terminal renderer module
//!
//! Renders the terminal grid to an RGBA pixel buffer using cached glyphs
//! and proper ANSI color support from alacritty_terminal.
//!
//! The pixel buffer uses RGBA byte order (A at offset 0, B at 1, G at 2, R at 3)
//! to match Minecraft's NativeImage internal format, avoiding per-pixel conversion
//! on the Java side.
//!
//! Ported from godot-alacritty with Godot types replaced by plain Rust types.
use crate::glyph_cache::{GlyphCache, GlyphStyle, SubpixelOrder};
/// Color with f32 components in 0.0..1.0 (stored as r, g, b, a internally;
/// converted to RGBA byte order when written to the pixel buffer)
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Color {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl Color {
pub fn from_rgb(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b, a: 1.0 }
}
pub fn from_rgba(r: f32, g: f32, b: f32, a: f32) -> Self {
Self { r, g, b, a }
}
}
/// Cursor style options
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CursorStyle {
/// Filled block cursor (default)
#[default]
Block = 0,
/// Underline cursor
Underline = 1,
/// Vertical bar cursor (I-beam)
Bar = 2,
}
impl From<i32> for CursorStyle {
fn from(value: i32) -> Self {
match value {
1 => CursorStyle::Underline,
2 => CursorStyle::Bar,
_ => CursorStyle::Block,
}
}
}
use alacritty_terminal::grid::Dimensions;
use alacritty_terminal::index::{Column, Line, Point};
use alacritty_terminal::term::cell::Flags;
use alacritty_terminal::term::Term;
use alacritty_terminal::vte::ansi::{Color as AnsiColor, NamedColor};
/// Default terminal color palette (based on Alacritty's default)
pub struct ColorPalette {
/// Normal colors (0-7)
pub normal: [Color; 8],
/// Bright colors (8-15)
pub bright: [Color; 8],
/// Default foreground
pub foreground: Color,
/// Default background
pub background: Color,
/// Cursor color
pub cursor: Color,
/// Selection highlight color
pub selection: Color,
}
impl Default for ColorPalette {
fn default() -> Self {
Self {
// Normal colors (dark)
normal: [
Color::from_rgb(0.0, 0.0, 0.0), // Black
Color::from_rgb(0.8, 0.0, 0.0), // Red
Color::from_rgb(0.0, 0.8, 0.0), // Green
Color::from_rgb(0.8, 0.8, 0.0), // Yellow
Color::from_rgb(0.0, 0.0, 0.8), // Blue
Color::from_rgb(0.8, 0.0, 0.8), // Magenta
Color::from_rgb(0.0, 0.8, 0.8), // Cyan
Color::from_rgb(0.75, 0.75, 0.75), // White
],
// Bright colors
bright: [
Color::from_rgb(0.5, 0.5, 0.5), // Bright Black (gray)
Color::from_rgb(1.0, 0.0, 0.0), // Bright Red
Color::from_rgb(0.0, 1.0, 0.0), // Bright Green
Color::from_rgb(1.0, 1.0, 0.0), // Bright Yellow
Color::from_rgb(0.3, 0.3, 1.0), // Bright Blue
Color::from_rgb(1.0, 0.0, 1.0), // Bright Magenta
Color::from_rgb(0.0, 1.0, 1.0), // Bright Cyan
Color::from_rgb(1.0, 1.0, 1.0), // Bright White
],
foreground: Color::from_rgb(0.9, 0.9, 0.9),
background: Color::from_rgb(0.1, 0.1, 0.12),
cursor: Color::from_rgb(0.0, 1.0, 0.5),
selection: Color::from_rgba(0.4, 0.6, 0.9, 0.5),
}
}
}
/// Terminal renderer using fontdue for glyph rasterization
pub struct TerminalRenderer {
/// Glyph cache
glyph_cache: GlyphCache,
/// Color palette
palette: ColorPalette,
/// Image buffer (RGBA pixels)
pixel_buffer: Vec<u8>,
/// Image dimensions
width: u32,
height: u32,
/// Terminal dimensions
cols: u32,
rows: u32,
/// Subpixel rendering order
subpixel_order: SubpixelOrder,
/// Dirty flag - set when content changes
dirty: bool,
}
impl TerminalRenderer {
/// Create a new terminal renderer with the embedded default font
pub fn new(cols: u32, rows: u32, font_size: f32) -> Result<Self, String> {
let glyph_cache = GlyphCache::new(font_size)?;
Self::from_glyph_cache(cols, rows, glyph_cache)
}
/// Create a new terminal renderer with custom spacing
pub fn with_spacing(
cols: u32,
rows: u32,
font_size: f32,
line_height: f32,
char_spacing: f32,
) -> Result<Self, String> {
let glyph_cache = GlyphCache::with_spacing(font_size, line_height, char_spacing)?;
Self::from_glyph_cache(cols, rows, glyph_cache)
}
/// Create a new terminal renderer with custom font data
pub fn from_font_data(
cols: u32,
rows: u32,
font_size: f32,
font_data: &[u8],
) -> Result<Self, String> {
let glyph_cache = GlyphCache::from_font_data(font_data, font_size)?;
Self::from_glyph_cache(cols, rows, glyph_cache)
}
/// Create a new terminal renderer with custom font data and spacing
pub fn from_font_data_with_spacing(
cols: u32,
rows: u32,
font_size: f32,
font_data: &[u8],
line_height: f32,
char_spacing: f32,
) -> Result<Self, String> {
let glyph_cache = GlyphCache::from_font_data_with_spacing(
font_data,
font_size,
line_height,
char_spacing,
)?;
Self::from_glyph_cache(cols, rows, glyph_cache)
}
/// Create renderer from a pre-built glyph cache
fn from_glyph_cache(cols: u32, rows: u32, glyph_cache: GlyphCache) -> Result<Self, String> {
let (cell_width, cell_height) = glyph_cache.cell_size();
let width = cols * cell_width;
let height = rows * cell_height;
let pixel_buffer = vec![0u8; (width * height * 4) as usize];
Ok(Self {
glyph_cache,
palette: ColorPalette::default(),
pixel_buffer,
width,
height,
cols,
rows,
subpixel_order: SubpixelOrder::None,
dirty: true,
})
}
/// Set the subpixel rendering order
pub fn set_subpixel_order(&mut self, order: SubpixelOrder) {
if self.subpixel_order != order {
self.subpixel_order = order;
self.glyph_cache.clear();
self.dirty = true;
}
}
/// Check and clear the dirty flag
pub fn take_dirty(&mut self) -> bool {
let was_dirty = self.dirty;
self.dirty = false;
was_dirty
}
/// Mark as dirty (content changed)
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
/// Render terminal content to the internal pixel buffer
pub fn render_with_cursor<T: alacritty_terminal::event::EventListener>(
&mut self,
term: &Term<T>,
cursor_style: CursorStyle,
cursor_visible: bool,
) {
let (cell_width, cell_height) = self.glyph_cache.cell_size();
let baseline = self.glyph_cache.baseline_offset();
// Clear to background color
self.fill_background();
let grid = term.grid();
// Get selection range if any
let selection_range = term.selection.as_ref().and_then(|sel| sel.to_range(term));
// Render each visible cell
let screen_lines = grid.screen_lines();
for line_idx in 0..screen_lines {
let line = Line(line_idx as i32);
let row = &grid[line];
for col_idx in 0..grid.columns() {
let cell = &row[Column(col_idx)];
let c = cell.c;
// Calculate cell position
let cell_x = col_idx as u32 * cell_width;
let cell_y = line_idx as u32 * cell_height;
// Check if this cell is selected
let point = Point::new(line, Column(col_idx));
let is_selected = selection_range
.as_ref()
.is_some_and(|range| range.contains(point));
// Get cell colors
let (fg_color, mut bg_color) = self.get_cell_colors(cell);
// If selected, swap fg/bg for visual indication
if is_selected {
bg_color = self.palette.selection;
}
// Draw cell background if different from terminal background
if bg_color != self.palette.background {
self.fill_rect(
cell_x as i32,
cell_y as i32,
cell_width as i32,
cell_height as i32,
bg_color,
);
}
// Skip rendering whitespace and control characters
if c == ' ' || c == '\0' || c == '\t' || c.is_control() {
continue;
}
// Get glyph style from cell flags
let style = GlyphStyle {
bold: cell.flags.contains(Flags::BOLD),
italic: cell.flags.contains(Flags::ITALIC),
};
// Get rasterized glyph
let use_subpixel = self.subpixel_order != SubpixelOrder::None;
let glyph = if use_subpixel {
self.glyph_cache.get_glyph_subpixel(c, style).clone()
} else {
self.glyph_cache.get_glyph(c, style).clone()
};
// Calculate glyph position within cell
let xmin = if glyph.is_subpixel {
glyph.metrics.xmin / 3
} else {
glyph.metrics.xmin
};
let glyph_x = cell_x as i32 + xmin;
let glyph_y = cell_y as i32 + baseline - glyph.metrics.ymin - glyph.height as i32;
// Render glyph
self.draw_glyph(&glyph, glyph_x, glyph_y, fg_color);
// Handle underline
if cell.flags.contains(Flags::UNDERLINE) {
let underline_y = cell_y as i32 + cell_height as i32 - 2;
self.fill_rect(cell_x as i32, underline_y, cell_width as i32, 1, fg_color);
}
// Handle strikethrough
if cell.flags.contains(Flags::STRIKEOUT) {
let strike_y = cell_y as i32 + cell_height as i32 / 2;
self.fill_rect(cell_x as i32, strike_y, cell_width as i32, 1, fg_color);
}
}
}
// Draw cursor (if visible)
if cursor_visible {
let cursor_point = grid.cursor.point;
let cursor_x = cursor_point.column.0 as u32 * cell_width;
let cursor_line_idx = (-cursor_point.line.0) as u32;
if cursor_line_idx < screen_lines as u32 {
let cursor_y = cursor_line_idx * cell_height;
self.draw_cursor(cursor_x, cursor_y, cell_width, cell_height, cursor_style);
}
}
self.dirty = true;
}
/// Get the pixel buffer as a byte slice
pub fn pixel_buffer(&self) -> &[u8] {
&self.pixel_buffer
}
/// Get foreground and background colors for a cell
fn get_cell_colors(&self, cell: &alacritty_terminal::term::cell::Cell) -> (Color, Color) {
let fg = self.ansi_to_color(&cell.fg);
let bg = self.ansi_to_color(&cell.bg);
// Handle inverse video
if cell.flags.contains(Flags::INVERSE) {
(bg, fg)
} else {
(fg, bg)
}
}
/// Convert Alacritty ANSI color to Color
fn ansi_to_color(&self, color: &AnsiColor) -> Color {
match color {
AnsiColor::Named(named) => self.named_to_color(*named),
AnsiColor::Spec(rgb) => Color::from_rgb(
rgb.r as f32 / 255.0,
rgb.g as f32 / 255.0,
rgb.b as f32 / 255.0,
),
AnsiColor::Indexed(idx) => self.indexed_to_color(*idx),
}
}
/// Convert named color to Color
fn named_to_color(&self, named: NamedColor) -> Color {
match named {
NamedColor::Black => self.palette.normal[0],
NamedColor::Red => self.palette.normal[1],
NamedColor::Green => self.palette.normal[2],
NamedColor::Yellow => self.palette.normal[3],
NamedColor::Blue => self.palette.normal[4],
NamedColor::Magenta => self.palette.normal[5],
NamedColor::Cyan => self.palette.normal[6],
NamedColor::White => self.palette.normal[7],
NamedColor::BrightBlack => self.palette.bright[0],
NamedColor::BrightRed => self.palette.bright[1],
NamedColor::BrightGreen => self.palette.bright[2],
NamedColor::BrightYellow => self.palette.bright[3],
NamedColor::BrightBlue => self.palette.bright[4],
NamedColor::BrightMagenta => self.palette.bright[5],
NamedColor::BrightCyan => self.palette.bright[6],
NamedColor::BrightWhite => self.palette.bright[7],
NamedColor::Foreground => self.palette.foreground,
NamedColor::Background => self.palette.background,
NamedColor::Cursor => self.palette.cursor,
_ => self.palette.foreground,
}
}
/// Convert 256-color index to Color
fn indexed_to_color(&self, idx: u8) -> Color {
match idx {
0..=7 => self.palette.normal[idx as usize],
8..=15 => self.palette.bright[(idx - 8) as usize],
16..=231 => {
// 6x6x6 color cube
let idx = idx - 16;
let r = (idx / 36) % 6;
let g = (idx / 6) % 6;
let b = idx % 6;
Color::from_rgb(
if r > 0 {
(r * 40 + 55) as f32 / 255.0
} else {
0.0
},
if g > 0 {
(g * 40 + 55) as f32 / 255.0
} else {
0.0
},
if b > 0 {
(b * 40 + 55) as f32 / 255.0
} else {
0.0
},
)
}
232..=255 => {
// Grayscale ramp
let gray = ((idx - 232) * 10 + 8) as f32 / 255.0;
Color::from_rgb(gray, gray, gray)
}
}
}
/// Fill background with terminal background color (RGBA byte order for memcpy to NativeImage)
fn fill_background(&mut self) {
let bg = self.palette.background;
let r = (bg.r * 255.0) as u8;
let g = (bg.g * 255.0) as u8;
let b = (bg.b * 255.0) as u8;
for i in (0..self.pixel_buffer.len()).step_by(4) {
self.pixel_buffer[i] = r;
self.pixel_buffer[i + 1] = g;
self.pixel_buffer[i + 2] = b;
self.pixel_buffer[i + 3] = 255;
}
}
/// Fill a rectangle with a color (RGBA byte order)
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
for py in y.max(0)..(y + h).min(self.height as i32) {
for px in x.max(0)..(x + w).min(self.width as i32) {
let idx = ((py as u32 * self.width + px as u32) * 4) as usize;
if idx + 3 < self.pixel_buffer.len() {
self.pixel_buffer[idx] = r;
self.pixel_buffer[idx + 1] = g;
self.pixel_buffer[idx + 2] = b;
self.pixel_buffer[idx + 3] = 255;
}
}
}
}
/// Draw a rasterized glyph at the given position
fn draw_glyph(
&mut self,
glyph: &crate::glyph_cache::RasterizedGlyph,
x: i32,
y: i32,
color: Color,
) {
if glyph.is_subpixel {
self.draw_glyph_subpixel(glyph, x, y, color);
} else {
self.draw_glyph_grayscale(glyph, x, y, color);
}
}
/// Draw a grayscale anti-aliased glyph (RGBA byte order)
fn draw_glyph_grayscale(
&mut self,
glyph: &crate::glyph_cache::RasterizedGlyph,
x: i32,
y: i32,
color: Color,
) {
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
for gy in 0..glyph.height as i32 {
let py = y + gy;
if py < 0 || py >= self.height as i32 {
continue;
}
for gx in 0..glyph.width as i32 {
let px = x + gx;
if px < 0 || px >= self.width as i32 {
continue;
}
let glyph_idx = (gy as u32 * glyph.width + gx as u32) as usize;
let alpha = glyph.bitmap[glyph_idx];
if alpha == 0 {
continue;
}
let idx = ((py as u32 * self.width + px as u32) * 4) as usize;
if idx + 3 >= self.pixel_buffer.len() {
continue;
}
// Alpha blending (RGBA: idx=R, idx+1=G, idx+2=B, idx+3=A)
let alpha_f = alpha as f32 / 255.0;
let inv_alpha = 1.0 - alpha_f;
self.pixel_buffer[idx] =
(r as f32 * alpha_f + self.pixel_buffer[idx] as f32 * inv_alpha) as u8;
self.pixel_buffer[idx + 1] =
(g as f32 * alpha_f + self.pixel_buffer[idx + 1] as f32 * inv_alpha) as u8;
self.pixel_buffer[idx + 2] =
(b as f32 * alpha_f + self.pixel_buffer[idx + 2] as f32 * inv_alpha) as u8;
self.pixel_buffer[idx + 3] = 255;
}
}
}
/// Draw a subpixel anti-aliased glyph (RGBA byte order)
fn draw_glyph_subpixel(
&mut self,
glyph: &crate::glyph_cache::RasterizedGlyph,
x: i32,
y: i32,
color: Color,
) {
let fg_r = (color.r * 255.0) as u8;
let fg_g = (color.g * 255.0) as u8;
let fg_b = (color.b * 255.0) as u8;
let subpixel_width = glyph.width * 3;
for gy in 0..glyph.height as i32 {
let py = y + gy;
if py < 0 || py >= self.height as i32 {
continue;
}
for gx in 0..glyph.width as i32 {
let px = x + gx;
if px < 0 || px >= self.width as i32 {
continue;
}
let glyph_base = (gy as u32 * subpixel_width + gx as u32 * 3) as usize;
if glyph_base + 2 >= glyph.bitmap.len() {
continue;
}
let (cov_r, cov_g, cov_b) = match self.subpixel_order {
SubpixelOrder::Rgb => (
glyph.bitmap[glyph_base],
glyph.bitmap[glyph_base + 1],
glyph.bitmap[glyph_base + 2],
),
SubpixelOrder::Bgr => (
glyph.bitmap[glyph_base + 2],
glyph.bitmap[glyph_base + 1],
glyph.bitmap[glyph_base],
),
SubpixelOrder::None => {
let avg = ((glyph.bitmap[glyph_base] as u16
+ glyph.bitmap[glyph_base + 1] as u16
+ glyph.bitmap[glyph_base + 2] as u16)
/ 3) as u8;
(avg, avg, avg)
}
};
if cov_r == 0 && cov_g == 0 && cov_b == 0 {
continue;
}
let idx = ((py as u32 * self.width + px as u32) * 4) as usize;
if idx + 3 >= self.pixel_buffer.len() {
continue;
}
// RGBA: idx=R, idx+1=G, idx+2=B, idx+3=A
let bg_r = self.pixel_buffer[idx];
let bg_g = self.pixel_buffer[idx + 1];
let bg_b = self.pixel_buffer[idx + 2];
let alpha_r = cov_r as f32 / 255.0;
let alpha_g = cov_g as f32 / 255.0;
let alpha_b = cov_b as f32 / 255.0;
self.pixel_buffer[idx] =
(fg_r as f32 * alpha_r + bg_r as f32 * (1.0 - alpha_r)) as u8;
self.pixel_buffer[idx + 1] =
(fg_g as f32 * alpha_g + bg_g as f32 * (1.0 - alpha_g)) as u8;
self.pixel_buffer[idx + 2] =
(fg_b as f32 * alpha_b + bg_b as f32 * (1.0 - alpha_b)) as u8;
self.pixel_buffer[idx + 3] = 255;
}
}
}
/// Draw cursor at the given cell position
fn draw_cursor(&mut self, x: u32, y: u32, w: u32, h: u32, style: CursorStyle) {
match style {
CursorStyle::Block => {
self.fill_rect(x as i32, y as i32, w as i32, h as i32, self.palette.cursor);
}
CursorStyle::Underline => {
let underline_height = 2.max(h / 8);
let underline_y = y + h - underline_height;
self.fill_rect(
x as i32,
underline_y as i32,
w as i32,
underline_height as i32,
self.palette.cursor,
);
}
CursorStyle::Bar => {
let bar_width = 2.max(w / 8);
self.fill_rect(
x as i32,
y as i32,
bar_width as i32,
h as i32,
self.palette.cursor,
);
}
}
}
/// Get the cell dimensions
pub fn cell_size(&self) -> (u32, u32) {
self.glyph_cache.cell_size()
}
/// Get the total dimensions
pub fn dimensions(&self) -> (u32, u32) {
(self.width, self.height)
}
/// Resize the renderer for new terminal dimensions
pub fn resize(&mut self, cols: u32, rows: u32) -> Result<(), String> {
let (cell_width, cell_height) = self.glyph_cache.cell_size();
self.cols = cols;
self.rows = rows;
self.width = cols * cell_width;
self.height = rows * cell_height;
self.pixel_buffer = vec![0u8; (self.width * self.height * 4) as usize];
self.dirty = true;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_palette_default() {
let palette = ColorPalette::default();
assert_eq!(palette.normal.len(), 8);
assert_eq!(palette.bright.len(), 8);
assert!(palette.foreground.r > 0.5);
assert!(palette.background.r < 0.5);
}
#[test]
fn test_renderer_creation() {
let renderer = TerminalRenderer::new(80, 24, 14.0);
assert!(renderer.is_ok());
let renderer = renderer.unwrap();
let (width, height) = renderer.dimensions();
assert!(width > 80);
assert!(height > 24);
}
#[test]
fn test_renderer_various_sizes() {
let test_cases = [(80, 24), (120, 40), (40, 12), (132, 43)];
for (cols, rows) in test_cases {
let renderer = TerminalRenderer::new(cols, rows, 14.0);
assert!(renderer.is_ok());
let renderer = renderer.unwrap();
let (cell_w, cell_h) = renderer.cell_size();
let (width, height) = renderer.dimensions();
assert_eq!(width, cols * cell_w);
assert_eq!(height, rows * cell_h);
}
}
#[test]
fn test_renderer_resize() {
let mut renderer = TerminalRenderer::new(80, 24, 14.0).unwrap();
let (orig_w, orig_h) = renderer.dimensions();
renderer.resize(120, 40).unwrap();
let (new_w, new_h) = renderer.dimensions();
assert!(new_w > orig_w);
assert!(new_h > orig_h);
}
#[test]
fn test_cell_size() {
let renderer = TerminalRenderer::new(80, 24, 14.0).unwrap();
let (cell_w, cell_h) = renderer.cell_size();
assert!(cell_w > 5);
assert!(cell_w < 20);
assert!(cell_h > 10);
assert!(cell_h < 30);
}
#[test]
fn test_indexed_color_256_palette() {
let renderer = TerminalRenderer::new(80, 24, 14.0).unwrap();
for idx in 0..=255u8 {
let color = renderer.indexed_to_color(idx);
assert!(color.r >= 0.0 && color.r <= 1.0);
assert!(color.g >= 0.0 && color.g <= 1.0);
assert!(color.b >= 0.0 && color.b <= 1.0);
}
}
#[test]
fn test_grayscale_progression() {
let renderer = TerminalRenderer::new(80, 24, 14.0).unwrap();
let mut prev_brightness = 0.0f32;
for idx in 232..=255u8 {
let color = renderer.indexed_to_color(idx);
assert!(color.r >= prev_brightness);
prev_brightness = color.r;
}
}
#[test]
fn test_cursor_style_from_i32() {
assert_eq!(CursorStyle::from(0), CursorStyle::Block);
assert_eq!(CursorStyle::from(1), CursorStyle::Underline);
assert_eq!(CursorStyle::from(2), CursorStyle::Bar);
assert_eq!(CursorStyle::from(-1), CursorStyle::Block);
}
#[test]
fn test_pixel_buffer_access() {
let renderer = TerminalRenderer::new(80, 24, 14.0).unwrap();
let (w, h) = renderer.dimensions();
assert_eq!(renderer.pixel_buffer().len(), (w * h * 4) as usize);
}
#[test]
fn test_dirty_flag() {
let mut renderer = TerminalRenderer::new(80, 24, 14.0).unwrap();
assert!(renderer.take_dirty()); // initially dirty
assert!(!renderer.take_dirty()); // cleared
renderer.mark_dirty();
assert!(renderer.take_dirty()); // dirty again
}
#[test]
fn test_pixel_buffer_rgba_format() {
let mut renderer = TerminalRenderer::new(10, 5, 14.0).unwrap();
// Fill a rect with a known color: R=255, G=0, B=128
let test_color = Color::from_rgb(1.0, 0.0, 0.5);
renderer.fill_rect(0, 0, 1, 1, test_color);
let pixels = renderer.pixel_buffer();
// RGBA byte order (matches NativeImage little-endian memory layout)
assert_eq!(pixels[0], 255, "offset 0 should be Red=255");
assert_eq!(pixels[1], 0, "offset 1 should be Green=0");
assert_eq!(pixels[2], 127, "offset 2 should be Blue=127");
assert_eq!(pixels[3], 255, "offset 3 should be Alpha=255");
}
}