smart rendering?
This commit is contained in:
@@ -6,7 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
|
||||
winit = { version = "0.30" }
|
||||
winit = { version = "0.30", features = ["wayland", "x11"] }
|
||||
wgpu = "25"
|
||||
glyphon = "0.9"
|
||||
|
||||
|
||||
349
README.md
349
README.md
@@ -1,3 +1,350 @@
|
||||
# simplicitty
|
||||
|
||||
A simple terminal emulator
|
||||
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,6 +2,7 @@ use std::sync::Arc;
|
||||
use tokio::runtime::Runtime;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
use winit::keyboard::{Key, NamedKey};
|
||||
use winit::window::Window;
|
||||
|
||||
mod rendering;
|
||||
@@ -54,11 +55,29 @@ impl winit::application::ApplicationHandler for Application {
|
||||
state.render().unwrap();
|
||||
}
|
||||
WindowEvent::KeyboardInput {
|
||||
device_id,
|
||||
device_id: _,
|
||||
event,
|
||||
is_synthetic,
|
||||
is_synthetic: _,
|
||||
} => {
|
||||
todo!()
|
||||
if event.state.is_pressed() {
|
||||
match event.logical_key {
|
||||
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(),
|
||||
_ => {}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
use std::sync::Arc;
|
||||
use wgpu::{CompositeAlphaMode, MultisampleState, TextureFormat};
|
||||
use wgpu::{
|
||||
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
|
||||
InstanceDescriptor, Queue, RenderPassColorAttachment, RenderPassDescriptor,
|
||||
RequestAdapterOptions, Surface, SurfaceConfiguration, SurfaceError, TextureFormat,
|
||||
TextureViewDescriptor,
|
||||
};
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::window::Window;
|
||||
|
||||
mod terminal_text;
|
||||
|
||||
pub struct State {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
surface: Surface<'static>,
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
config: SurfaceConfiguration,
|
||||
|
||||
size: winit::dpi::PhysicalSize<u32>,
|
||||
size: PhysicalSize<u32>,
|
||||
|
||||
text: terminal_text::TerminalText,
|
||||
|
||||
@@ -18,7 +24,7 @@ pub struct State {
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>) {
|
||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||
if new_size.width > 0 && new_size.height > 0 {
|
||||
self.size = new_size;
|
||||
self.config.width = new_size.width;
|
||||
@@ -28,27 +34,24 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> Result<(), wgpu::SurfaceError> {
|
||||
// Use a longer text string to better demonstrate wrapping
|
||||
self.text.set_text("This is a long text string that should wrap when the window is resized. The terminal emulator should handle text wrapping properly to ensure that all text is visible and properly formatted regardless of the window dimensions. This demonstrates the text wrapping functionality using glyphon library.\n\nMultiple paragraphs should also work correctly with proper wrapping behavior.");
|
||||
|
||||
pub fn render(&mut self) -> Result<(), SurfaceError> {
|
||||
// Prepare with current configuration (which includes the current window size)
|
||||
self.text.prepare(&self.device, &self.queue, &self.config);
|
||||
|
||||
let output = self.surface.get_current_texture()?;
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
.create_view(&TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
.create_command_encoder(&CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
let mut _render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
color_attachments: &[Some(RenderPassColorAttachment {
|
||||
view: &view,
|
||||
resolve_target: None,
|
||||
ops: wgpu::Operations {
|
||||
@@ -74,11 +77,10 @@ impl State {
|
||||
|
||||
pub async fn new(window: Arc<Window>) -> Self {
|
||||
let size = window.inner_size();
|
||||
let scale_factor = window.scale_factor();
|
||||
|
||||
// The instance is needed to create an `Adapter` and a `Surface`
|
||||
// Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
let instance = Instance::new(&InstanceDescriptor {
|
||||
backends: wgpu::Backends::PRIMARY,
|
||||
..Default::default()
|
||||
});
|
||||
@@ -104,7 +106,7 @@ impl State {
|
||||
// .unwrap()
|
||||
//
|
||||
let adapter = instance
|
||||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||||
.request_adapter(&RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
@@ -113,7 +115,7 @@ impl State {
|
||||
.unwrap();
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::DeviceDescriptor {
|
||||
.request_device(&DeviceDescriptor {
|
||||
// The features field allows to specify the extra features we want
|
||||
// The graphics card you have limits the features you can use. If you want to use certain
|
||||
// features, you may need to limit what devices you support or provide workarounds.
|
||||
@@ -146,7 +148,7 @@ impl State {
|
||||
// .unwrap_or(surface_caps.formats[0]); // We get the first sRGB format
|
||||
|
||||
let surface_format = TextureFormat::Bgra8UnormSrgb;
|
||||
let config = wgpu::SurfaceConfiguration {
|
||||
let config = SurfaceConfiguration {
|
||||
// This field describes how SurfaceTextures will be used. RENDER_ATTACHMENT
|
||||
// specifies that the textures will be used to write to the screen.
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||||
@@ -184,4 +186,16 @@ impl State {
|
||||
window,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_text(&mut self, text: &str) {
|
||||
for c in text.chars() {
|
||||
self.text.insert_char(c);
|
||||
}
|
||||
self.window.request_redraw();
|
||||
}
|
||||
|
||||
pub fn handle_enter(&mut self) {
|
||||
self.text.handle_enter();
|
||||
self.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use glyphon::{
|
||||
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
|
||||
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
||||
Attrs, Buffer, BufferLine, Cache, Color, Cursor, Family, FontSystem, Metrics, Resolution,
|
||||
Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
||||
};
|
||||
|
||||
use wgpu::{Device, MultisampleState, Queue, RenderPass, TextureFormat};
|
||||
use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat};
|
||||
|
||||
pub struct TerminalText {
|
||||
font_system: FontSystem,
|
||||
@@ -12,8 +12,12 @@ pub struct TerminalText {
|
||||
atlas: TextAtlas,
|
||||
buffer: Buffer,
|
||||
renderer: TextRenderer,
|
||||
dirty_regions: Vec<TextBounds>,
|
||||
cursor: Cursor,
|
||||
}
|
||||
|
||||
use log::info;
|
||||
|
||||
impl TerminalText {
|
||||
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText {
|
||||
let mut font_system = FontSystem::new();
|
||||
@@ -38,6 +42,12 @@ impl TerminalText {
|
||||
atlas,
|
||||
buffer,
|
||||
renderer,
|
||||
dirty_regions: vec![],
|
||||
cursor: Cursor {
|
||||
line: 0,
|
||||
index: 0,
|
||||
affinity: glyphon::Affinity::After,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,12 +74,7 @@ impl TerminalText {
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
}
|
||||
|
||||
pub fn prepare(
|
||||
&mut self,
|
||||
device: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
config: &wgpu::SurfaceConfiguration,
|
||||
) {
|
||||
pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) {
|
||||
// Update viewport with new dimensions
|
||||
self.viewport.update(
|
||||
queue,
|
||||
@@ -82,30 +87,57 @@ impl TerminalText {
|
||||
// Make sure text is properly wrapped for the current width
|
||||
self.resize(config.width, config.height);
|
||||
|
||||
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),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
if !self.dirty_regions.is_empty() {
|
||||
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();
|
||||
}
|
||||
} 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),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
self.dirty_regions.clear();
|
||||
}
|
||||
|
||||
pub fn render(&self, render_pass: &mut RenderPass) {
|
||||
@@ -117,4 +149,127 @@ impl TerminalText {
|
||||
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;
|
||||
}
|
||||
info!("line nr: {line}");
|
||||
|
||||
let current_line = self.buffer.lines[line].text().to_string();
|
||||
let pos = self.cursor.index.min(current_line.len());
|
||||
|
||||
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 attrs = self.buffer.lines[line].attrs_list().clone();
|
||||
|
||||
// Update the buffer with the new text
|
||||
self.buffer.lines[line].set_text(&new_text, ending, attrs);
|
||||
|
||||
// Update cursor position
|
||||
self.cursor.index = pos + 1;
|
||||
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
}
|
||||
|
||||
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) {
|
||||
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 = ¤t_line[..pos];
|
||||
let text_after = ¤t_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);
|
||||
|
||||
// Insert the new line after the current one
|
||||
self.buffer.lines.insert(line + 1, new_line);
|
||||
|
||||
self.mark_all_dirty();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
pub fn mark_all_dirty(&mut self) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
src/main.rs
10
src/main.rs
@@ -1,8 +1,18 @@
|
||||
use log::info;
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
mod app;
|
||||
|
||||
fn main() {
|
||||
// Initialize logger
|
||||
env_logger::init();
|
||||
|
||||
// Log platform information
|
||||
info!("Starting simplicitty terminal emulator");
|
||||
|
||||
// Create event loop with explicit backend preference
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
|
||||
// Run the application
|
||||
event_loop.run_app(&mut app::Application::new()).unwrap();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user