font rendering

This commit is contained in:
Zacharias-Brohn
2025-12-16 16:55:10 +01:00
parent f304fd18a8
commit 5c3eee3448
14 changed files with 4060 additions and 874 deletions
+126 -16
View File
@@ -1,5 +1,6 @@
//! Terminal state management and escape sequence handling.
use crate::graphics::{GraphicsCommand, ImageStorage};
use crate::keyboard::{query_response, KeyboardState};
use crate::vt_parser::{CsiParams, Handler, Parser};
@@ -320,15 +321,12 @@ pub struct ScrollbackBuffer {
}
impl ScrollbackBuffer {
/// Creates a new scrollback buffer with the given capacity and column width.
/// All lines are pre-allocated to avoid any allocation during scrolling.
pub fn new(capacity: usize, cols: usize) -> Self {
// Pre-allocate all lines upfront
let lines = if capacity > 0 {
(0..capacity).map(|_| vec![Cell::default(); cols]).collect()
} else {
Vec::new()
};
/// Creates a new scrollback buffer with the given capacity.
/// Lines are allocated lazily as needed to avoid slow startup.
pub fn new(capacity: usize) -> Self {
// Don't pre-allocate lines - allocate them lazily as content is added
// This avoids allocating and zeroing potentially 20MB+ of memory at startup
let lines = Vec::with_capacity(capacity.min(1024)); // Start with reasonable capacity
Self {
lines,
@@ -361,9 +359,9 @@ impl ScrollbackBuffer {
/// If the buffer is full, the oldest line is overwritten and its slot is returned
/// for reuse (the caller can swap content into it).
///
/// This is the key operation - it's O(1) with just modulo arithmetic, no allocation.
/// Lines are allocated lazily on first use to avoid slow startup.
#[inline]
pub fn push(&mut self) -> &mut Vec<Cell> {
pub fn push(&mut self, cols: usize) -> &mut Vec<Cell> {
if self.capacity == 0 {
// Shouldn't happen in normal use, but handle gracefully
panic!("Cannot push to zero-capacity scrollback buffer");
@@ -379,7 +377,11 @@ impl ScrollbackBuffer {
self.start = (self.start + 1) % self.capacity;
// count stays the same
} else {
// Buffer not full yet - just increment count
// Buffer not full yet - allocate new line if needed
if idx >= self.lines.len() {
// Grow the lines vector and allocate the new line
self.lines.push(vec![Cell::default(); cols]);
}
self.count += 1;
}
@@ -488,6 +490,12 @@ pub struct Terminal {
/// Command queue for terminal-to-application communication.
/// Commands are added by OSC handlers and consumed by the application.
command_queue: Vec<TerminalCommand>,
/// Image storage for Kitty graphics protocol.
pub image_storage: ImageStorage,
/// Cell width in pixels (for image sizing).
pub cell_width: f32,
/// Cell height in pixels (for image sizing).
pub cell_height: f32,
}
impl Terminal {
@@ -529,7 +537,7 @@ impl Terminal {
keyboard: KeyboardState::new(),
response_queue: Vec::new(),
palette: ColorPalette::default(),
scrollback: ScrollbackBuffer::new(scrollback_limit, cols),
scrollback: ScrollbackBuffer::new(scrollback_limit),
scroll_offset: 0,
mouse_tracking: MouseTrackingMode::default(),
mouse_encoding: MouseEncoding::default(),
@@ -545,6 +553,9 @@ impl Terminal {
parser: Some(Parser::new()),
stats: ProcessingStats::default(),
command_queue: Vec::new(),
image_storage: ImageStorage::new(),
cell_width: 10.0, // Default, will be set by renderer
cell_height: 20.0, // Default, will be set by renderer
}
}
@@ -808,7 +819,8 @@ impl Terminal {
if self.scroll_top == 0 && !self.using_alternate_screen && self.scrollback.capacity > 0 {
// Get a slot in the ring buffer - this is O(1) with just modulo arithmetic
// If buffer is full, this overwrites the oldest line (perfect for our swap)
let dest = self.scrollback.push();
let cols = self.cols;
let dest = self.scrollback.push(cols);
// Swap grid row content into scrollback slot
// The scrollback slot's old content (if any) moves to the grid row
std::mem::swap(&mut self.grid[recycled_grid_row], dest);
@@ -1184,7 +1196,8 @@ impl Terminal {
for visual_row in 0..self.rows {
let grid_row = self.line_map[visual_row];
// Get a slot in the ring buffer and swap content into it
let dest = self.scrollback.push();
let cols = self.cols;
let dest = self.scrollback.push(cols);
std::mem::swap(&mut self.grid[grid_row], dest);
}
}
@@ -1403,6 +1416,12 @@ impl Handler for Terminal {
}
}
/// Handle an APC (Application Program Command) sequence.
/// Used for Kitty graphics protocol.
fn apc(&mut self, data: &[u8]) {
self.handle_apc(data);
}
/// Handle a complete CSI sequence.
fn csi(&mut self, params: &CsiParams) {
let action = params.final_char as char;
@@ -1619,10 +1638,32 @@ impl Handler for Terminal {
self.cursor_row = 0;
self.cursor_col = 0;
}
// Window manipulation (CSI Ps t)
// Window manipulation (CSI Ps t) - XTWINOPS
't' => {
let ps = params.get(0, 0);
match ps {
14 => {
// Report text area size in pixels: CSI 4 ; height ; width t
let pixel_height = (self.rows as f32 * self.cell_height) as u32;
let pixel_width = (self.cols as f32 * self.cell_width) as u32;
let response = format!("\x1b[4;{};{}t", pixel_height, pixel_width);
self.response_queue.extend_from_slice(response.as_bytes());
log::debug!("XTWINOPS 14: Reported text area size {}x{} pixels", pixel_width, pixel_height);
}
16 => {
// Report cell size in pixels: CSI 6 ; height ; width t
let cell_h = self.cell_height as u32;
let cell_w = self.cell_width as u32;
let response = format!("\x1b[6;{};{}t", cell_h, cell_w);
self.response_queue.extend_from_slice(response.as_bytes());
log::debug!("XTWINOPS 16: Reported cell size {}x{} pixels", cell_w, cell_h);
}
18 => {
// Report text area size in characters: CSI 8 ; rows ; cols t
let response = format!("\x1b[8;{};{}t", self.rows, self.cols);
self.response_queue.extend_from_slice(response.as_bytes());
log::debug!("XTWINOPS 18: Reported text area size {}x{} chars", self.cols, self.rows);
}
22 | 23 => {
// Save/restore window title - ignore
}
@@ -2079,4 +2120,73 @@ impl Terminal {
}
}
}
/// Set cell dimensions (called by renderer after font metrics are calculated).
pub fn set_cell_size(&mut self, width: f32, height: f32) {
self.cell_width = width;
self.cell_height = height;
}
/// Handle an APC (Application Program Command) sequence.
/// This is used for the Kitty graphics protocol.
fn handle_apc(&mut self, data: &[u8]) {
// Kitty graphics protocol: APC starts with 'G'
if let Some(cmd) = GraphicsCommand::parse(data) {
log::debug!(
"Graphics command: action={:?} format={:?} id={:?} size={}x{:?} C={} U={}",
cmd.action,
cmd.format,
cmd.image_id,
cmd.width.unwrap_or(0),
cmd.height,
cmd.cursor_movement,
cmd.unicode_placeholder
);
// Convert cursor_row to absolute row (accounting for scrollback)
// This allows images to scroll with terminal content
let absolute_row = self.scrollback.len() + self.cursor_row;
// Process the command
let (response, placement_result) = self.image_storage.process_command(
cmd,
self.cursor_col,
absolute_row,
self.cell_width,
self.cell_height,
);
// Queue the response to send back to the application
if let Some(resp) = response {
self.response_queue.extend_from_slice(resp.as_bytes());
}
// Move cursor after image placement per Kitty protocol spec:
// "After placing an image on the screen the cursor must be moved to the
// right by the number of cols in the image placement rectangle and down
// by the number of rows in the image placement rectangle."
// However, if C=1 was specified, don't move the cursor.
if let Some(placement) = placement_result {
if !placement.suppress_cursor_move && !placement.virtual_placement {
// Move cursor down by (rows - 1) since we're already on the first row
// Then set cursor to the column after the image
let new_row = self.cursor_row + placement.rows.saturating_sub(1);
if new_row >= self.rows {
// Need to scroll
let scroll_amount = new_row - self.rows + 1;
self.scroll_up(scroll_amount);
self.cursor_row = self.rows - 1;
} else {
self.cursor_row = new_row;
}
// Move cursor to after the image (or stay at column 0 of next line)
// Per protocol, cursor ends at the last row of the image
log::debug!(
"Cursor moved after image placement: row={} (moved {} rows)",
self.cursor_row, placement.rows.saturating_sub(1)
);
}
}
}
}
}