fixed some issues with text rendering
This commit is contained in:
350
README.md
350
README.md
@@ -1,350 +0,0 @@
|
|||||||
# simplicitty
|
|
||||||
|
|
||||||
A simple terminal emulator
|
|
||||||
|
|
||||||
## 1. Implement Incremental Rendering
|
|
||||||
|
|
||||||
Currently, your renderer appears to redraw the entire terminal content on each
|
|
||||||
frame. For a terminal emulator, this is inefficient since only small portions
|
|
||||||
typically change at once.
|
|
||||||
|
|
||||||
rust // Add to TerminalText struct dirty_regions: Vec<Rectangle>, // Store
|
|
||||||
regions that need updating last_frame_content: String, // Store previous frame's
|
|
||||||
content
|
|
||||||
|
|
||||||
Only redraw areas that have changed since the last frame by tracking dirty
|
|
||||||
regions.
|
|
||||||
|
|
||||||
## 2. Implement Text Caching
|
|
||||||
|
|
||||||
The current implementation reshapes all text on every frame. Instead:
|
|
||||||
|
|
||||||
rust // Add to TerminalText cached_lines: HashMap<String, CachedTextLine>,
|
|
||||||
|
|
||||||
Cache shaped text lines and only reshape when content changes or window resizes.
|
|
||||||
|
|
||||||
## 3. Use Double Buffering for Text
|
|
||||||
|
|
||||||
Maintain two buffers - one for the current frame and one for the next:
|
|
||||||
|
|
||||||
rust // In TerminalText front_buffer: Buffer, back_buffer: Buffer,
|
|
||||||
|
|
||||||
Prepare the back buffer while rendering the front buffer, then swap them.
|
|
||||||
|
|
||||||
## 4. Optimize Atlas Trimming
|
|
||||||
|
|
||||||
The trim() call after every frame can be expensive. Consider:
|
|
||||||
|
|
||||||
rust // In TerminalText frames_since_trim: u32,
|
|
||||||
|
|
||||||
pub fn trim(&mut self) { self.frames_since_trim += 1; if
|
|
||||||
self.frames_since_trim > 30 { // Only trim every 30 frames self.atlas.trim();
|
|
||||||
self.frames_since_trim = 0; } }
|
|
||||||
|
|
||||||
## 5. Implement View Culling
|
|
||||||
|
|
||||||
Only render text that's actually visible:
|
|
||||||
|
|
||||||
rust // In prepare() method let visible_area = TextBounds { left: 0, top:
|
|
||||||
scroll_position, right: config.width as i32, bottom: scroll_position +
|
|
||||||
config.height as i32, };
|
|
||||||
|
|
||||||
## 6. Use Asynchronous Text Preparation
|
|
||||||
|
|
||||||
Move text shaping to a background thread:
|
|
||||||
|
|
||||||
rust // In Application struct text_preparation_pool: ThreadPool,
|
|
||||||
|
|
||||||
// Then queue text preparation tasks self.text_preparation_pool.spawn(move || {
|
|
||||||
// Shape text here // Signal completion via channel });
|
|
||||||
|
|
||||||
## 7. Optimize GPU Memory Usage
|
|
||||||
|
|
||||||
The current implementation might be creating new GPU resources frequently.
|
|
||||||
Implement resource pooling:
|
|
||||||
|
|
||||||
rust // In TerminalText texture_pool: Vec<wgpu::Texture>, // Reuse textures
|
|
||||||
instead of creating new ones
|
|
||||||
|
|
||||||
## 8. Implement Partial Updates for the Text Atlas
|
|
||||||
|
|
||||||
Instead of rebuilding the entire text atlas, update only changed glyphs:
|
|
||||||
|
|
||||||
rust // Add method to TextAtlas pub fn update_region(&mut self, device: &Device,
|
|
||||||
queue: &Queue, region: Rectangle) { // Update only the specified region }
|
|
||||||
|
|
||||||
## 9. Use Compute Shaders for Text Processing
|
|
||||||
|
|
||||||
For complex text operations, consider using compute shaders:
|
|
||||||
|
|
||||||
rust // Create a compute pipeline for text processing let compute_pipeline =
|
|
||||||
device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor { // Configure
|
|
||||||
for text processing });
|
|
||||||
|
|
||||||
## 10. Implement Frame Skipping
|
|
||||||
|
|
||||||
Skip rendering frames when nothing has changed:
|
|
||||||
|
|
||||||
rust pub fn render(&mut self) -> Result<(), SurfaceError> { if !self.has_changes
|
|
||||||
{ return Ok(()); } // Render as normal }
|
|
||||||
|
|
||||||
## 11. Profile and Optimize Hot Paths
|
|
||||||
|
|
||||||
Use a profiler to identify bottlenecks in your rendering pipeline. Common issues
|
|
||||||
might be: • Excessive GPU state changes • Inefficient text shaping • Unnecessary
|
|
||||||
buffer updates
|
|
||||||
|
|
||||||
## 12. Implement Proper Vsync Control
|
|
||||||
|
|
||||||
Your current implementation uses PresentMode::Fifo (VSync), which is good for
|
|
||||||
power efficiency but might not be optimal for all cases:
|
|
||||||
|
|
||||||
rust // Allow configurable present mode based on user preference present_mode:
|
|
||||||
if vsync_enabled { wgpu::PresentMode::Fifo } else { wgpu::PresentMode::Immediate
|
|
||||||
},
|
|
||||||
|
|
||||||
Based on your project structure and the README.md optimization suggestions,
|
|
||||||
here's how you can implement incremental text buffer updates:
|
|
||||||
|
|
||||||
## 1. Track Changes with Dirty Regions
|
|
||||||
|
|
||||||
First, modify your TerminalText struct to track which parts of the text have
|
|
||||||
changed:
|
|
||||||
|
|
||||||
rust pub struct TerminalText { font_system: FontSystem, cache: SwashCache,
|
|
||||||
viewport: Viewport, atlas: TextAtlas, buffer: Buffer, renderer: TextRenderer,
|
|
||||||
|
|
||||||
// Add these fields to track changes
|
|
||||||
last_content: String,
|
|
||||||
dirty_regions: Vec<TextBounds>,
|
|
||||||
cursor_position: usize,
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
## 2. Implement Incremental Text Updates
|
|
||||||
|
|
||||||
Instead of replacing the entire text on each update, implement methods to
|
|
||||||
insert, delete, or modify text at specific positions:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
impl TerminalText { // Add a character at the cursor position pub fn
|
|
||||||
insert_char(&mut self, c: char) { let current_text =
|
|
||||||
self.buffer.text().to_string(); let pos =
|
|
||||||
self.cursor_position.min(current_text.len());
|
|
||||||
|
|
||||||
// Create new text with the character inserted
|
|
||||||
let mut new_text = current_text.clone();
|
|
||||||
new_text.insert(pos, c);
|
|
||||||
|
|
||||||
// Calculate the affected region
|
|
||||||
let affected_line = self.calculate_line_for_position(pos);
|
|
||||||
let line_bounds = self.get_line_bounds(affected_line);
|
|
||||||
self.dirty_regions.push(line_bounds);
|
|
||||||
|
|
||||||
// Update the buffer with the new text
|
|
||||||
self.buffer.set_text(
|
|
||||||
&mut self.font_system,
|
|
||||||
&new_text,
|
|
||||||
&Attrs::new().family(Family::Monospace),
|
|
||||||
Shaping::Advanced,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update cursor position
|
|
||||||
self.cursor_position = pos + 1;
|
|
||||||
self.last_content = new_text;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete a character before the cursor
|
|
||||||
pub fn delete_char(&mut self) {
|
|
||||||
let current_text = self.buffer.text().to_string();
|
|
||||||
if self.cursor_position > 0 && !current_text.is_empty() {
|
|
||||||
let pos = self.cursor_position.min(current_text.len());
|
|
||||||
|
|
||||||
// Create new text with the character deleted
|
|
||||||
let mut new_text = current_text.clone();
|
|
||||||
new_text.remove(pos - 1);
|
|
||||||
|
|
||||||
// Calculate the affected region
|
|
||||||
let affected_line = self.calculate_line_for_position(pos - 1);
|
|
||||||
let line_bounds = self.get_line_bounds(affected_line);
|
|
||||||
self.dirty_regions.push(line_bounds);
|
|
||||||
|
|
||||||
// Update the buffer with the new text
|
|
||||||
self.buffer.set_text(
|
|
||||||
&mut self.font_system,
|
|
||||||
&new_text,
|
|
||||||
&Attrs::new().family(Family::Monospace),
|
|
||||||
Shaping::Advanced,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update cursor position
|
|
||||||
self.cursor_position = pos - 1;
|
|
||||||
self.last_content = new_text;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods to calculate affected regions
|
|
||||||
fn calculate_line_for_position(&self, pos: usize) -> usize {
|
|
||||||
// Count newlines up to the position
|
|
||||||
self.buffer.text()[..pos].matches('\n').count()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_line_bounds(&self, line: usize) -> TextBounds {
|
|
||||||
// Calculate the bounds for a specific line
|
|
||||||
// This is a simplified version - you'll need to implement this based on your text layout
|
|
||||||
TextBounds {
|
|
||||||
left: 0,
|
|
||||||
top: (line * self.get_line_height()) as i32,
|
|
||||||
right: self.viewport.width() as i32,
|
|
||||||
bottom: ((line + 1) * self.get_line_height()) as i32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_line_height(&self) -> usize {
|
|
||||||
// Get the line height based on your font metrics
|
|
||||||
self.buffer.metrics().line_height.ceil() as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 3. Optimize the Rendering Process
|
|
||||||
|
|
||||||
Modify your prepare and render methods to only update the dirty regions:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
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, }, );
|
|
||||||
|
|
||||||
// Make sure text is properly wrapped for the current width
|
|
||||||
self.resize(config.width, config.height);
|
|
||||||
|
|
||||||
// Only reshape if there are dirty regions
|
|
||||||
if !self.dirty_regions.is_empty() {
|
|
||||||
// Prepare only the dirty regions
|
|
||||||
for region in &self.dirty_regions {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear dirty regions after preparing
|
|
||||||
self.dirty_regions.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. Implement a Double Buffer Approach
|
|
||||||
|
|
||||||
To further optimize, implement a double buffer approach:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
pub struct TerminalText { // ... existing fields front_buffer: Buffer,
|
|
||||||
back_buffer: Buffer, buffer_swap_needed: bool, }
|
|
||||||
|
|
||||||
impl TerminalText { // ... existing methods
|
|
||||||
|
|
||||||
pub fn swap_buffers(&mut self) {
|
|
||||||
if self.buffer_swap_needed {
|
|
||||||
std::mem::swap(&mut self.front_buffer, &mut self.back_buffer);
|
|
||||||
self.buffer_swap_needed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prepare_back_buffer(&mut self, device: &Device, queue: &Queue) {
|
|
||||||
// Prepare the back buffer while the front buffer is being rendered
|
|
||||||
if !self.dirty_regions.is_empty() {
|
|
||||||
// Apply changes to the back buffer
|
|
||||||
// ...
|
|
||||||
|
|
||||||
self.buffer_swap_needed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Implement Cursor Rendering
|
|
||||||
|
|
||||||
Add cursor rendering to provide visual feedback:
|
|
||||||
|
|
||||||
rust pub fn render_cursor(&mut self, render_pass: &mut RenderPass) { //
|
|
||||||
Calculate cursor position in screen coordinates let (x, y) =
|
|
||||||
self.get_cursor_screen_position();
|
|
||||||
|
|
||||||
// Render cursor using a simple rectangle
|
|
||||||
// You'll need to implement this based on your rendering system
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_cursor_screen_position(&self) -> (f32, f32) { // Calculate screen
|
|
||||||
position based on cursor_position // This will depend on your text layout system
|
|
||||||
// ... }
|
|
||||||
|
|
||||||
## 6. Handle Special Keys
|
|
||||||
|
|
||||||
In your keyboard event handler, handle special keys like backspace, enter, and
|
|
||||||
arrow keys:
|
|
||||||
|
|
||||||
rust fn handle*key_event(&mut self, key_event: KeyEvent) { match key_event.key {
|
|
||||||
Key::Character(c) => { if key_event.state == ElementState::Pressed {
|
|
||||||
self.text.insert_char(c.chars().next().unwrap_or(' '));
|
|
||||||
self.window.request_redraw(); } }, Key::Named(NamedKey::Backspace) => { if
|
|
||||||
key_event.state == ElementState::Pressed { self.text.delete_char();
|
|
||||||
self.window.request_redraw(); } }, Key::Named(NamedKey::Enter) => { if
|
|
||||||
key_event.state == ElementState::Pressed { self.text.insert_char('\n');
|
|
||||||
self.window.request_redraw(); } }, // Handle arrow keys for cursor movement
|
|
||||||
Key::Named(NamedKey::ArrowLeft) => { if key_event.state == ElementState::Pressed
|
|
||||||
{ self.text.move_cursor_left(); self.window.request_redraw(); } },
|
|
||||||
Key::Named(NamedKey::ArrowRight) => { if key_event.state ==
|
|
||||||
ElementState::Pressed { self.text.move_cursor_right();
|
|
||||||
self.window.request_redraw(); } }, * => {} } }
|
|
||||||
|
|
||||||
## 7. Implement Frame Skipping
|
|
||||||
|
|
||||||
Skip rendering frames when nothing has changed:
|
|
||||||
|
|
||||||
rust pub fn render(&mut self) -> Result<(), SurfaceError> { // Skip rendering if
|
|
||||||
nothing has changed if self.dirty_regions.is_empty() && !self.buffer_swap_needed
|
|
||||||
{ return Ok(()); }
|
|
||||||
|
|
||||||
// Swap buffers if needed
|
|
||||||
self.text.swap_buffers();
|
|
||||||
|
|
||||||
// Render as normal
|
|
||||||
// ...
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
These changes will significantly improve the efficiency of your text rendering
|
|
||||||
by:
|
|
||||||
|
|
||||||
1. Only updating parts of the text that have changed
|
|
||||||
2. Using double buffering to prepare updates while rendering
|
|
||||||
3. Skipping frames when nothing has changed
|
|
||||||
4. Optimizing the text atlas updates
|
|
||||||
|
|
||||||
This approach aligns with the optimization suggestions in your README.md and
|
|
||||||
should provide a much more efficient text rendering system for your terminal
|
|
||||||
emulator.
|
|
||||||
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
|||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use winit::dpi::LogicalSize;
|
use winit::dpi::LogicalSize;
|
||||||
use winit::event::WindowEvent;
|
use winit::event::WindowEvent;
|
||||||
use winit::keyboard::{Key, NamedKey};
|
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
mod rendering;
|
mod rendering;
|
||||||
@@ -60,23 +59,7 @@ impl winit::application::ApplicationHandler for Application {
|
|||||||
is_synthetic: _,
|
is_synthetic: _,
|
||||||
} => {
|
} => {
|
||||||
if event.state.is_pressed() {
|
if event.state.is_pressed() {
|
||||||
match event.logical_key {
|
state.handle_keyboard_input(&event);
|
||||||
Key::Named(k) => match k {
|
|
||||||
NamedKey::Enter => {
|
|
||||||
state.handle_enter();
|
|
||||||
}
|
|
||||||
_ => todo!(),
|
|
||||||
},
|
|
||||||
Key::Character(c) => {
|
|
||||||
state.add_text(&event.text.unwrap());
|
|
||||||
}
|
|
||||||
Key::Dead(_) => {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
Key::Unidentified(_) => {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
WindowEvent::CloseRequested => event_loop.exit(),
|
WindowEvent::CloseRequested => event_loop.exit(),
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use wgpu::{
|
|||||||
TextureViewDescriptor,
|
TextureViewDescriptor,
|
||||||
};
|
};
|
||||||
use winit::dpi::PhysicalSize;
|
use winit::dpi::PhysicalSize;
|
||||||
|
use winit::event::KeyEvent;
|
||||||
|
use winit::keyboard::{Key, NamedKey};
|
||||||
use winit::window::Window;
|
use winit::window::Window;
|
||||||
|
|
||||||
mod terminal_text;
|
mod terminal_text;
|
||||||
@@ -30,6 +32,7 @@ impl State {
|
|||||||
self.config.width = new_size.width;
|
self.config.width = new_size.width;
|
||||||
self.config.height = new_size.height;
|
self.config.height = new_size.height;
|
||||||
self.surface.configure(&self.device, &self.config);
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
self.text.resize(new_size.width, new_size.height);
|
||||||
self.window.request_redraw();
|
self.window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -187,15 +190,26 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_text(&mut self, text: &str) {
|
pub fn handle_keyboard_input(&mut self, event: &KeyEvent) {
|
||||||
for c in text.chars() {
|
match &event.logical_key {
|
||||||
|
Key::Named(k) => match k {
|
||||||
|
NamedKey::Enter => {
|
||||||
|
self.text.handle_enter();
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
},
|
||||||
|
Key::Character(s) => {
|
||||||
|
for c in s.chars() {
|
||||||
self.text.insert_char(c);
|
self.text.insert_char(c);
|
||||||
}
|
}
|
||||||
self.window.request_redraw();
|
|
||||||
}
|
}
|
||||||
|
Key::Dead(_) => {
|
||||||
pub fn handle_enter(&mut self) {
|
todo!()
|
||||||
self.text.handle_enter();
|
}
|
||||||
|
Key::Unidentified(_) => {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
self.window.request_redraw();
|
self.window.request_redraw();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use glyphon::{
|
use glyphon::{
|
||||||
Attrs, Buffer, BufferLine, Cache, Color, Cursor, Family, FontSystem, Metrics, Resolution,
|
Buffer, BufferLine, Cache, Color, Cursor, FontSystem, Metrics, Resolution, Shaping, SwashCache,
|
||||||
Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
||||||
};
|
};
|
||||||
|
|
||||||
use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat};
|
use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat};
|
||||||
@@ -16,10 +16,11 @@ pub struct TerminalText {
|
|||||||
cursor: Cursor,
|
cursor: Cursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
use log::info;
|
use log::{debug, trace};
|
||||||
|
|
||||||
impl TerminalText {
|
impl TerminalText {
|
||||||
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> 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 mut font_system = FontSystem::new();
|
||||||
let swash_cache = SwashCache::new();
|
let swash_cache = SwashCache::new();
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ impl TerminalText {
|
|||||||
let mut buffer = Buffer::new(&mut font_system, Metrics::relative(13.0, 1.25));
|
let mut buffer = Buffer::new(&mut font_system, Metrics::relative(13.0, 1.25));
|
||||||
buffer.set_wrap(&mut font_system, Wrap::Glyph);
|
buffer.set_wrap(&mut font_system, Wrap::Glyph);
|
||||||
|
|
||||||
|
trace!("Loading system fonts");
|
||||||
// Add a default font
|
// Add a default font
|
||||||
font_system.db_mut().load_system_fonts();
|
font_system.db_mut().load_system_fonts();
|
||||||
|
|
||||||
@@ -51,27 +53,42 @@ impl TerminalText {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_text(&mut self, text: &str) {
|
|
||||||
// Clear the buffer and set new text
|
|
||||||
self.buffer.set_text(
|
|
||||||
&mut self.font_system,
|
|
||||||
text,
|
|
||||||
&Attrs::new().family(Family::Monospace),
|
|
||||||
Shaping::Advanced,
|
|
||||||
);
|
|
||||||
self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn resize(&mut self, width: u32, height: u32) {
|
pub fn resize(&mut self, width: u32, height: u32) {
|
||||||
// Update the buffer's wrapping based on the new width
|
trace!("Resizing window - Width: {width} Height: {height}");
|
||||||
self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph);
|
|
||||||
self.buffer.set_size(
|
self.buffer.set_size(
|
||||||
&mut self.font_system,
|
&mut self.font_system,
|
||||||
Some(width as f32),
|
Some(width as f32),
|
||||||
Some(height 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
|
// Reshape the text with the new dimensions
|
||||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
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) {
|
pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) {
|
||||||
@@ -84,11 +101,9 @@ impl TerminalText {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Make sure text is properly wrapped for the current width
|
|
||||||
self.resize(config.width, config.height);
|
|
||||||
|
|
||||||
if !self.dirty_regions.is_empty() {
|
if !self.dirty_regions.is_empty() {
|
||||||
for region in &self.dirty_regions {
|
let region = self.merge_dirty_regions();
|
||||||
|
trace!("Preparing region {:?}", region);
|
||||||
self.renderer
|
self.renderer
|
||||||
.prepare(
|
.prepare(
|
||||||
device,
|
device,
|
||||||
@@ -101,34 +116,7 @@ impl TerminalText {
|
|||||||
left: 0.0,
|
left: 0.0,
|
||||||
top: 0.0,
|
top: 0.0,
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
bounds: *region,
|
bounds: region,
|
||||||
default_color: Color::rgb(255, 255, 255),
|
|
||||||
custom_glyphs: &[],
|
|
||||||
}],
|
|
||||||
&mut self.cache,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If no dirty regions, prepare the entire buffer (first render or after resize)
|
|
||||||
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: TextBounds {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
right: config.width as i32,
|
|
||||||
bottom: config.height as i32,
|
|
||||||
},
|
|
||||||
default_color: Color::rgb(255, 255, 255),
|
default_color: Color::rgb(255, 255, 255),
|
||||||
custom_glyphs: &[],
|
custom_glyphs: &[],
|
||||||
}],
|
}],
|
||||||
@@ -157,47 +145,21 @@ impl TerminalText {
|
|||||||
// TODO
|
// TODO
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info!("line nr: {line}");
|
|
||||||
|
|
||||||
let current_line = self.buffer.lines[line].text().to_string();
|
let mut text = self.buffer.lines[line].text().to_string().clone();
|
||||||
let pos = self.cursor.index.min(current_line.len());
|
let pos = self.cursor.index.min(text.len());
|
||||||
|
text.insert(pos, c);
|
||||||
info!("cursor position: {pos}");
|
|
||||||
|
|
||||||
info!("Line text: {current_line}");
|
|
||||||
|
|
||||||
let mut new_text = current_line.clone();
|
|
||||||
new_text.insert(pos, c);
|
|
||||||
|
|
||||||
let line_bounds = self.get_line_bounds(line);
|
|
||||||
self.dirty_regions.push(line_bounds);
|
|
||||||
|
|
||||||
let ending = self.buffer.lines[line].ending();
|
let ending = self.buffer.lines[line].ending();
|
||||||
let attrs = self.buffer.lines[line].attrs_list().clone();
|
let attrs = self.buffer.lines[line].attrs_list().clone();
|
||||||
|
|
||||||
// Update the buffer with the new text
|
// Update the buffer with the new text
|
||||||
self.buffer.lines[line].set_text(&new_text, ending, attrs);
|
self.buffer.lines[line].set_text(&text, ending, attrs);
|
||||||
|
|
||||||
// Update cursor position
|
// Update cursor position
|
||||||
self.cursor.index = pos + 1;
|
self.cursor.index = pos + 1;
|
||||||
|
|
||||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
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
|
|
||||||
// This is a simplified version - you'll need to implement this based on your text layout
|
|
||||||
TextBounds {
|
|
||||||
left: 0,
|
|
||||||
top: (line * self.get_line_height()) as i32,
|
|
||||||
right: self.viewport.resolution().width as i32,
|
|
||||||
bottom: ((line + 1) * self.get_line_height()) as i32,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_line_height(&self) -> usize {
|
|
||||||
// Get the line height based on your font metrics
|
|
||||||
self.buffer.metrics().line_height.ceil() as usize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_enter(&mut self) {
|
pub fn handle_enter(&mut self) {
|
||||||
@@ -223,53 +185,119 @@ impl TerminalText {
|
|||||||
|
|
||||||
// Create a new line with text after cursor
|
// Create a new line with text after cursor
|
||||||
let new_line = BufferLine::new(text_after, ending, attrs, Shaping::Advanced);
|
let new_line = BufferLine::new(text_after, ending, attrs, Shaping::Advanced);
|
||||||
|
|
||||||
// Insert the new line after the current one
|
|
||||||
self.buffer.lines.insert(line + 1, new_line);
|
self.buffer.lines.insert(line + 1, new_line);
|
||||||
|
|
||||||
self.mark_all_dirty();
|
|
||||||
|
|
||||||
// Update cursor position to beginning of next line
|
// Update cursor position to beginning of next line
|
||||||
self.cursor.line = line + 1;
|
self.cursor.line = line + 1;
|
||||||
self.cursor.index = 0;
|
self.cursor.index = 0;
|
||||||
|
|
||||||
// Reshape the buffer
|
// Reshape the buffer
|
||||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||||
|
self.ensure_visible_text_rendered();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn mark_all_dirty(&mut self) {
|
fn get_line_bounds(&self, line: usize) -> TextBounds {
|
||||||
// Create a single region covering the entire viewport
|
// Calculate the bounds for a specific line
|
||||||
let full_bounds = TextBounds {
|
|
||||||
left: 0,
|
let line_height = self.buffer.metrics().line_height;
|
||||||
top: 0,
|
let viewport_width = self.viewport.resolution().width;
|
||||||
right: self.viewport.resolution().width as i32,
|
|
||||||
bottom: self.viewport.resolution().height as i32,
|
// 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
|
||||||
};
|
};
|
||||||
self.dirty_regions = vec![full_bounds];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn move_cursor_left(&mut self) {
|
let bottom = if bottom_f32 > i32::MAX as f32 {
|
||||||
if self.cursor.index > 0 {
|
i32::MAX
|
||||||
self.cursor.index -= 1;
|
} else if bottom_f32 < i32::MIN as f32 {
|
||||||
} else if self.cursor.line > 0 {
|
i32::MIN
|
||||||
// Move to end of previous line
|
} else {
|
||||||
self.cursor.line -= 1;
|
bottom_f32 as i32
|
||||||
let prev_line_len = self.buffer.lines[self.cursor.line].text().len();
|
};
|
||||||
self.cursor.index = prev_line_len;
|
|
||||||
|
// 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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_cursor_right(&mut self) {
|
fn get_visible_line_range(&self) -> (usize, usize) {
|
||||||
if self.cursor.line < self.buffer.lines.len() {
|
let viewport_height = self.viewport.resolution().height as f32;
|
||||||
let current_line_len = self.buffer.lines[self.cursor.line].text().len();
|
let line_height = self.buffer.metrics().line_height;
|
||||||
|
|
||||||
if self.cursor.index < current_line_len {
|
// Start from line 0 (no scrolling yet)
|
||||||
self.cursor.index += 1;
|
let start_line = 0;
|
||||||
} else if self.cursor.line < self.buffer.lines.len() - 1 {
|
|
||||||
// Move to beginning of next line
|
// Calculate how many complete lines fit in the viewport
|
||||||
self.cursor.line += 1;
|
// Add 1 to include partially visible lines at the bottom
|
||||||
self.cursor.index = 0;
|
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;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user