Compare commits
2 Commits
main
...
3715ecf841
| Author | SHA1 | Date | |
|---|---|---|---|
| 3715ecf841 | |||
| 1ec12fa728 |
23
.gitignore
vendored
23
.gitignore
vendored
@@ -1,22 +1 @@
|
|||||||
# ---> Rust
|
/target
|
||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
|
||||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
|
||||||
Cargo.lock
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
|
||||||
|
|
||||||
# RustRover
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|||||||
2875
Cargo.lock
generated
Normal file
2875
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "simplicitty"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
tokio = { version = "1.44", features = ["full"] }
|
||||||
|
|
||||||
|
winit = { version = "0.30", features = ["wayland", "x11"] }
|
||||||
|
wgpu = "25"
|
||||||
|
glyphon = "0.9"
|
||||||
|
|
||||||
|
termion = "4"
|
||||||
|
|
||||||
|
env_logger = "0.10"
|
||||||
|
log = "0.4"
|
||||||
347
README.md
347
README.md
@@ -1,3 +1,350 @@
|
|||||||
# simplicitty
|
# 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.
|
||||||
|
|||||||
86
src/app/mod.rs
Normal file
86
src/app/mod.rs
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
pub struct Application {
|
||||||
|
window_state: Option<rendering::State>,
|
||||||
|
runtime: Runtime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
window_state: None,
|
||||||
|
runtime: Runtime::new().expect("Failed to create tokio runtime"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl winit::application::ApplicationHandler for Application {
|
||||||
|
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
|
||||||
|
if self.window_state.is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up window
|
||||||
|
let (width, height) = (800, 600);
|
||||||
|
let window_attributes = Window::default_attributes()
|
||||||
|
.with_inner_size(LogicalSize::new(width as f64, height as f64))
|
||||||
|
.with_title("Terminal emulator");
|
||||||
|
let window = Arc::new(event_loop.create_window(window_attributes).unwrap());
|
||||||
|
|
||||||
|
self.window_state = Some(self.runtime.block_on(rendering::State::new(window)));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_event(
|
||||||
|
&mut self,
|
||||||
|
event_loop: &winit::event_loop::ActiveEventLoop,
|
||||||
|
_window_id: winit::window::WindowId,
|
||||||
|
event: WindowEvent,
|
||||||
|
) {
|
||||||
|
let Some(state) = &mut self.window_state else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
match event {
|
||||||
|
WindowEvent::Resized(size) => {
|
||||||
|
state.resize(size);
|
||||||
|
}
|
||||||
|
WindowEvent::RedrawRequested => {
|
||||||
|
state.render().unwrap();
|
||||||
|
}
|
||||||
|
WindowEvent::KeyboardInput {
|
||||||
|
device_id: _,
|
||||||
|
event,
|
||||||
|
is_synthetic: _,
|
||||||
|
} => {
|
||||||
|
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(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
201
src/app/rendering/mod.rs
Normal file
201
src/app/rendering/mod.rs
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
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: Surface<'static>,
|
||||||
|
device: Device,
|
||||||
|
queue: Queue,
|
||||||
|
config: SurfaceConfiguration,
|
||||||
|
|
||||||
|
size: PhysicalSize<u32>,
|
||||||
|
|
||||||
|
text: terminal_text::TerminalText,
|
||||||
|
|
||||||
|
window: Arc<Window>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
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;
|
||||||
|
self.config.height = new_size.height;
|
||||||
|
self.surface.configure(&self.device, &self.config);
|
||||||
|
self.window.request_redraw();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(&TextureViewDescriptor::default());
|
||||||
|
let mut encoder = self
|
||||||
|
.device
|
||||||
|
.create_command_encoder(&CommandEncoderDescriptor {
|
||||||
|
label: Some("Render Encoder"),
|
||||||
|
});
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut _render_pass = encoder.begin_render_pass(&RenderPassDescriptor {
|
||||||
|
label: Some("Render Pass"),
|
||||||
|
color_attachments: &[Some(RenderPassColorAttachment {
|
||||||
|
view: &view,
|
||||||
|
resolve_target: None,
|
||||||
|
ops: wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
|
||||||
|
store: wgpu::StoreOp::Store,
|
||||||
|
},
|
||||||
|
})],
|
||||||
|
depth_stencil_attachment: None,
|
||||||
|
timestamp_writes: None,
|
||||||
|
occlusion_query_set: None,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.text.render(&mut _render_pass);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.submit(Some(encoder.finish()));
|
||||||
|
output.present();
|
||||||
|
|
||||||
|
self.text.trim();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn new(window: Arc<Window>) -> Self {
|
||||||
|
let size = window.inner_size();
|
||||||
|
|
||||||
|
// The instance is needed to create an `Adapter` and a `Surface`
|
||||||
|
// Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
|
||||||
|
let instance = Instance::new(&InstanceDescriptor {
|
||||||
|
backends: wgpu::Backends::PRIMARY,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is the part of the window where we draw
|
||||||
|
// The window needs to implement the `HasRawWindowHandle` trait
|
||||||
|
// We also need the surface to request the `Adapter`
|
||||||
|
let surface = instance
|
||||||
|
.create_surface(window.clone())
|
||||||
|
.expect("Failed to create surface");
|
||||||
|
|
||||||
|
// The adapter is the handle to our graphics card. We can get information about the device
|
||||||
|
// with this. We will use it to create the `Device` and `Queue` later
|
||||||
|
// We could enumerate the adapters and find the one that suits us:
|
||||||
|
//
|
||||||
|
// let adapter = instance
|
||||||
|
// .enumerate_adapters(wgpu::Backends::all())
|
||||||
|
// .filter(|adapter| {
|
||||||
|
// // Check if this adapter supports our surface
|
||||||
|
// adapter.is_surface_supported(&surface)
|
||||||
|
// })
|
||||||
|
// .next()
|
||||||
|
// .unwrap()
|
||||||
|
//
|
||||||
|
let adapter = instance
|
||||||
|
.request_adapter(&RequestAdapterOptions {
|
||||||
|
power_preference: wgpu::PowerPreference::default(),
|
||||||
|
compatible_surface: Some(&surface),
|
||||||
|
force_fallback_adapter: false,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let (device, queue) = adapter
|
||||||
|
.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.
|
||||||
|
// You can get a list of features supported by your device using `adapter.features()` or
|
||||||
|
// `device.features()`.
|
||||||
|
// See: https://docs.rs/wgpu/latest/wgpu/struct.Features.html
|
||||||
|
required_features: wgpu::Features::empty(),
|
||||||
|
// The limits field describes the limits of certain types of resources we can
|
||||||
|
// create. See: https://docs.rs/wgpu/latest/wgpu/struct.Limits.html
|
||||||
|
required_limits: wgpu::Limits::default(),
|
||||||
|
label: None,
|
||||||
|
// Provides the adapter with the preferred memory allocation strategy.
|
||||||
|
// See: https://wgpu.rs/doc/wgpu/enum.MemoryHints.html
|
||||||
|
memory_hints: Default::default(),
|
||||||
|
trace: wgpu::Trace::Off,
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// let surface_caps = surface.get_capabilities(&adapter);
|
||||||
|
// // This will define how the surface creates its underlying `SurfaceTexture`
|
||||||
|
// // This assumes an sRGB surface texture. Using a different one will result in all the colors
|
||||||
|
// // coming out darker. If we want to support non sRGB surfaces, we'll need to account for that
|
||||||
|
// // when drawing to the frame.
|
||||||
|
// let surface_format = surface_caps
|
||||||
|
// .formats
|
||||||
|
// .iter()
|
||||||
|
// .find(|f| f.is_srgb())
|
||||||
|
// .copied()
|
||||||
|
// .unwrap_or(surface_caps.formats[0]); // We get the first sRGB format
|
||||||
|
|
||||||
|
let surface_format = TextureFormat::Bgra8UnormSrgb;
|
||||||
|
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,
|
||||||
|
// The format defines how SurfaceTextures will be stored on the GPU.
|
||||||
|
// We can get a supported format from the SurfaceCapabilities
|
||||||
|
format: surface_format,
|
||||||
|
width: size.width, // Must be > 0
|
||||||
|
height: size.height, // Must be > 0
|
||||||
|
// Takes a `wgpu::PresentMode` option, which determines how to sync the surface with
|
||||||
|
// the display. `PresentMode::Fifo` is VSync.
|
||||||
|
// A list of available modes can be obtained with `&surface_caps.present_modes`
|
||||||
|
// See: https://docs.rs/wgpu/latest/wgpu/enum.PresentMode.html
|
||||||
|
present_mode: wgpu::PresentMode::Fifo,
|
||||||
|
// ???
|
||||||
|
alpha_mode: CompositeAlphaMode::Opaque,
|
||||||
|
//
|
||||||
|
view_formats: vec![],
|
||||||
|
desired_maximum_frame_latency: 2,
|
||||||
|
};
|
||||||
|
surface.configure(&device, &config);
|
||||||
|
|
||||||
|
// Set up text renderer
|
||||||
|
let text = terminal_text::TerminalText::new(&device, &queue, surface_format);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
surface,
|
||||||
|
device,
|
||||||
|
queue,
|
||||||
|
config,
|
||||||
|
|
||||||
|
size,
|
||||||
|
|
||||||
|
text,
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
275
src/app/rendering/terminal_text/mod.rs
Normal file
275
src/app/rendering/terminal_text/mod.rs
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
use glyphon::{
|
||||||
|
Attrs, Buffer, BufferLine, Cache, Color, Cursor, Family, 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::info;
|
||||||
|
|
||||||
|
impl TerminalText {
|
||||||
|
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText {
|
||||||
|
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);
|
||||||
|
|
||||||
|
// 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 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) {
|
||||||
|
// Update the buffer's wrapping based on the new width
|
||||||
|
self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph);
|
||||||
|
self.buffer.set_size(
|
||||||
|
&mut self.font_system,
|
||||||
|
Some(width as f32),
|
||||||
|
Some(height as f32),
|
||||||
|
);
|
||||||
|
// Reshape the text with the new dimensions
|
||||||
|
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/main.rs
Normal file
18
src/main.rs
Normal file
@@ -0,0 +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