Files
simplicitty/src/app/rendering/terminal_text/mod.rs

304 lines
9.7 KiB
Rust

use glyphon::{
Buffer, BufferLine, Cache, Color, Cursor, FontSystem, Metrics, Resolution, Shaping, SwashCache,
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
};
use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat};
pub struct TerminalText {
font_system: FontSystem,
cache: SwashCache,
viewport: Viewport,
atlas: TextAtlas,
buffer: Buffer,
renderer: TextRenderer,
dirty_regions: Vec<TextBounds>,
cursor: Cursor,
}
use log::{debug, trace};
impl TerminalText {
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText {
debug!("Creating a new TerminalText object");
let mut font_system = FontSystem::new();
let swash_cache = SwashCache::new();
let cache = Cache::new(device);
let viewport = Viewport::new(device, &cache);
let mut atlas = TextAtlas::new(device, queue, &cache, surface_format);
let renderer = TextRenderer::new(&mut atlas, device, MultisampleState::default(), None);
let mut buffer = Buffer::new(&mut font_system, Metrics::relative(13.0, 1.25));
buffer.set_wrap(&mut font_system, Wrap::Glyph);
trace!("Loading system fonts");
// Add a default font
font_system.db_mut().load_system_fonts();
Self {
font_system,
cache: swash_cache,
viewport,
atlas,
buffer,
renderer,
dirty_regions: vec![],
cursor: Cursor {
line: 0,
index: 0,
affinity: glyphon::Affinity::After,
},
}
}
pub fn resize(&mut self, width: u32, height: u32) {
trace!("Resizing window - Width: {width} Height: {height}");
self.buffer.set_size(
&mut self.font_system,
Some(width as f32),
Some(height as f32),
);
// Update the buffer's wrapping based on the new width
self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph);
// Reshape the text with the new dimensions
self.buffer.shape_until_scroll(&mut self.font_system, false);
self.ensure_visible_text_rendered();
}
fn merge_dirty_regions(&self) -> TextBounds {
TextBounds {
left: self
.dirty_regions
.iter()
.min_by_key(|b| b.left)
.unwrap()
.left,
top: self.dirty_regions.iter().min_by_key(|b| b.top).unwrap().top,
right: self
.dirty_regions
.iter()
.max_by_key(|b| b.right)
.unwrap()
.right,
bottom: self
.dirty_regions
.iter()
.max_by_key(|b| b.bottom)
.unwrap()
.bottom,
}
}
pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) {
// Update viewport with new dimensions
self.viewport.update(
queue,
Resolution {
width: config.width,
height: config.height,
},
);
if !self.dirty_regions.is_empty() {
let region = self.merge_dirty_regions();
trace!("Preparing region {:?}", region);
self.renderer
.prepare(
device,
queue,
&mut self.font_system,
&mut self.atlas,
&self.viewport,
[TextArea {
buffer: &self.buffer,
left: 0.0,
top: 0.0,
scale: 1.0,
bounds: region,
default_color: Color::rgb(255, 255, 255),
custom_glyphs: &[],
}],
&mut self.cache,
)
.unwrap();
}
self.dirty_regions.clear();
}
pub fn render(&self, render_pass: &mut RenderPass) {
self.renderer
.render(&self.atlas, &self.viewport, render_pass)
.unwrap();
}
pub fn trim(&mut self) {
self.atlas.trim()
}
pub fn insert_char(&mut self, c: char) {
let line = self.cursor.line;
if line >= self.buffer.lines.len() {
// TODO
return;
}
let mut text = self.buffer.lines[line].text().to_string().clone();
let pos = self.cursor.index.min(text.len());
text.insert(pos, c);
let ending = self.buffer.lines[line].ending();
let attrs = self.buffer.lines[line].attrs_list().clone();
// Update the buffer with the new text
self.buffer.lines[line].set_text(&text, ending, attrs);
// Update cursor position
self.cursor.index = pos + 1;
self.buffer.shape_until_scroll(&mut self.font_system, false);
self.ensure_visible_text_rendered();
}
pub fn handle_enter(&mut self) {
let line = self.cursor.line;
// Ensure line is within bounds
if line >= self.buffer.lines.len() {
return;
}
// Get the current line text
let current_line = self.buffer.lines[line].text().to_string();
let pos = self.cursor.index.min(current_line.len());
// Split the line at cursor position
let text_before = &current_line[..pos];
let text_after = &current_line[pos..];
// Update the current line with text before cursor
let ending = self.buffer.lines[line].ending();
let attrs = self.buffer.lines[line].attrs_list().clone();
self.buffer.lines[line].set_text(text_before, ending, attrs.clone());
// Create a new line with text after cursor
let new_line = BufferLine::new(text_after, ending, attrs, Shaping::Advanced);
self.buffer.lines.insert(line + 1, new_line);
// Update cursor position to beginning of next line
self.cursor.line = line + 1;
self.cursor.index = 0;
// Reshape the buffer
self.buffer.shape_until_scroll(&mut self.font_system, false);
self.ensure_visible_text_rendered();
}
fn get_line_bounds(&self, line: usize) -> TextBounds {
// Calculate the bounds for a specific line
let line_height = self.buffer.metrics().line_height;
let viewport_width = self.viewport.resolution().width;
// Calculate floating point values first
let top_f32 = (line as f32 * line_height).floor();
let bottom_f32 = ((line as f32 + 1.0) * line_height).ceil();
// Safe conversions with overflow checks
let top = if top_f32 > i32::MAX as f32 {
i32::MAX
} else if top_f32 < i32::MIN as f32 {
i32::MIN
} else {
top_f32 as i32
};
let bottom = if bottom_f32 > i32::MAX as f32 {
i32::MAX
} else if bottom_f32 < i32::MIN as f32 {
i32::MIN
} else {
bottom_f32 as i32
};
// Ensure viewport width doesn't exceed i32::MAX
let right = viewport_width.min(i32::MAX as u32) as i32;
TextBounds {
left: 0,
top,
right,
bottom,
}
}
fn get_visible_line_range(&self) -> (usize, usize) {
let viewport_height = self.viewport.resolution().height as f32;
let line_height = self.buffer.metrics().line_height;
// Start from line 0 (no scrolling yet)
let start_line = 0;
// Calculate how many complete lines fit in the viewport
// Add 1 to include partially visible lines at the bottom
let visible_lines = (viewport_height / line_height).ceil() as usize + 1;
// Make sure we don't go beyond the actual number of lines
let end_line = if self.buffer.lines.is_empty() {
0
} else {
(start_line + visible_lines).min(self.buffer.lines.len())
};
trace!("visible line range goes from {start_line} to {end_line}");
(start_line, end_line)
}
pub fn ensure_visible_text_rendered(&mut self) {
let (start_line, end_line) = self.get_visible_line_range();
for i in start_line..end_line {
self.dirty_regions.push(self.get_line_bounds(i));
}
}
// pub fn mark_all_dirty(&mut self) {
// trace!("Marking all regions as dirty");
// // Create a single region covering the entire viewport
// let full_bounds = TextBounds {
// left: 0,
// top: 0,
// right: self.viewport.resolution().width as i32,
// bottom: self.viewport.resolution().height as i32,
// };
// self.dirty_regions = vec![full_bounds];
// }
// pub fn move_cursor_left(&mut self) {
// if self.cursor.index > 0 {
// self.cursor.index -= 1;
// } else if self.cursor.line > 0 {
// // Move to end of previous line
// self.cursor.line -= 1;
// let prev_line_len = self.buffer.lines[self.cursor.line].text().len();
// self.cursor.index = prev_line_len;
// }
// }
// pub fn move_cursor_right(&mut self) {
// if self.cursor.line < self.buffer.lines.len() {
// let current_line_len = self.buffer.lines[self.cursor.line].text().len();
// if self.cursor.index < current_line_len {
// self.cursor.index += 1;
// } else if self.cursor.line < self.buffer.lines.len() - 1 {
// // Move to beginning of next line
// self.cursor.line += 1;
// self.cursor.index = 0;
// }
// }
// }
}