Compare commits
7 Commits
3715ecf841
...
legacy/fir
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ba5a52c14 | |||
| e5ce246760 | |||
| a3fa7eb9d2 | |||
| fdc923621e | |||
| ebd3d4008b | |||
| ae56f78075 | |||
| b0ecc5623c |
41
AmazonQ.md
Normal file
41
AmazonQ.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# Text Scrolling Implementation in Simplicitty
|
||||
|
||||
This document outlines the implementation of text scrolling in the Simplicitty terminal emulator project.
|
||||
|
||||
## Overview
|
||||
|
||||
The implementation adds the ability to scroll through text content when it doesn't fit completely in the window. The scrolling is implemented on a line-by-line basis, allowing users to navigate through the content using keyboard shortcuts.
|
||||
|
||||
## Key Components
|
||||
|
||||
1. **Scroll State Management**:
|
||||
- Added `scroll_offset` to track the current scroll position (in visual lines)
|
||||
- Added `max_scroll_offset` to limit scrolling to available content
|
||||
|
||||
2. **Viewport Calculation**:
|
||||
- Modified `get_visible_line_range()` to consider the scroll offset when determining visible lines
|
||||
- Added `update_max_scroll_offset()` to recalculate the maximum scroll position based on content and viewport size
|
||||
|
||||
3. **Scrolling Methods**:
|
||||
- `scroll_up(lines)`: Move viewport up by specified number of lines
|
||||
- `scroll_down(lines)`: Move viewport down by specified number of lines
|
||||
- `page_up()`: Scroll up by one page (viewport height)
|
||||
- `page_down()`: Scroll down by one page (viewport height)
|
||||
|
||||
4. **Keyboard Controls**:
|
||||
- Ctrl+Up/Down: Scroll one line up/down
|
||||
- Page Up/Down: Scroll one page up/down
|
||||
- Ctrl+Home/End: Scroll to top/bottom of content
|
||||
|
||||
## Implementation Details
|
||||
|
||||
The scrolling implementation works by adjusting the `scroll_offset` value, which determines which visual lines are rendered in the viewport. The `get_visible_line_range()` function uses this offset to calculate which logical lines should be rendered.
|
||||
|
||||
When the window is resized or content changes, the `max_scroll_offset` is recalculated to ensure scrolling remains within valid bounds.
|
||||
|
||||
## Future Improvements
|
||||
|
||||
1. Add mouse wheel support for scrolling
|
||||
2. Implement smooth scrolling animations
|
||||
3. Add a visual scrollbar indicator
|
||||
4. Preserve horizontal scroll position when navigating vertically
|
||||
357
README.md
357
README.md
@@ -1,350 +1,11 @@
|
||||
# simplicitty
|
||||
# Simplicitty
|
||||
|
||||
A simple terminal emulator
|
||||
## Roadmap
|
||||
|
||||
## 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.
|
||||
1. [ ] Complete keyboard input handling (arrow keys, backspace, etc.)
|
||||
2. [ ] Implement a PTY interface to spawn and communicate with processes
|
||||
3. [ ] Add support for terminal protocols (ANSI escape sequences)
|
||||
4. [ ] Implement scrollback buffer and viewport scrolling
|
||||
5. [ ] Add color and text styling support
|
||||
6. [ ] Implement clipboard integration
|
||||
7. [ ] Add configuration options
|
||||
|
||||
@@ -2,7 +2,6 @@ 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;
|
||||
@@ -59,25 +58,10 @@ impl winit::application::ApplicationHandler for Application {
|
||||
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!()
|
||||
}
|
||||
}
|
||||
}
|
||||
state.handle_keyboard_input(&event);
|
||||
}
|
||||
WindowEvent::ModifiersChanged(modifiers) => {
|
||||
state.update_modifiers(modifiers.state());
|
||||
}
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
_ => {}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use log::{debug, trace};
|
||||
use std::sync::Arc;
|
||||
use wgpu::{
|
||||
CommandEncoderDescriptor, CompositeAlphaMode, Device, DeviceDescriptor, Instance,
|
||||
@@ -6,6 +7,8 @@ use wgpu::{
|
||||
TextureViewDescriptor,
|
||||
};
|
||||
use winit::dpi::PhysicalSize;
|
||||
use winit::event::{ElementState, KeyEvent};
|
||||
use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey};
|
||||
use winit::window::Window;
|
||||
|
||||
mod terminal_text;
|
||||
@@ -21,20 +24,28 @@ pub struct State {
|
||||
text: terminal_text::TerminalText,
|
||||
|
||||
window: Arc<Window>,
|
||||
|
||||
// Keyboard state tracking
|
||||
modifiers: ModifiersState,
|
||||
dead_key_state: Option<char>,
|
||||
caps_lock_enabled: bool,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn resize(&mut self, new_size: PhysicalSize<u32>) {
|
||||
trace!("Resizing window");
|
||||
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.text.resize(new_size.width, new_size.height);
|
||||
self.window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self) -> Result<(), SurfaceError> {
|
||||
trace!("Rendering requested");
|
||||
// Prepare with current configuration (which includes the current window size)
|
||||
self.text.prepare(&self.device, &self.queue, &self.config);
|
||||
|
||||
@@ -64,14 +75,13 @@ impl State {
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
|
||||
trace!("Rendering...");
|
||||
self.text.render(&mut _render_pass);
|
||||
}
|
||||
|
||||
self.queue.submit(Some(encoder.finish()));
|
||||
output.present();
|
||||
|
||||
self.text.trim();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -184,18 +194,200 @@ impl State {
|
||||
text,
|
||||
|
||||
window,
|
||||
|
||||
// Initialize keyboard state
|
||||
modifiers: ModifiersState::empty(),
|
||||
dead_key_state: None,
|
||||
caps_lock_enabled: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_text(&mut self, text: &str) {
|
||||
for c in text.chars() {
|
||||
self.text.insert_char(c);
|
||||
pub fn update_modifiers(&mut self, state: ModifiersState) {
|
||||
self.modifiers = state;
|
||||
debug!("Modifiers updated: {:?}", self.modifiers);
|
||||
}
|
||||
|
||||
pub fn handle_keyboard_input(&mut self, event: &KeyEvent) {
|
||||
// Only process key presses, not releases (except for tracking caps lock)
|
||||
if event.state == ElementState::Released {
|
||||
// Special case for caps lock which toggles on release
|
||||
if let Key::Named(NamedKey::CapsLock) = event.logical_key {
|
||||
self.caps_lock_enabled = !self.caps_lock_enabled;
|
||||
debug!("Caps lock toggled: {}", self.caps_lock_enabled);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match &event.logical_key {
|
||||
Key::Named(k) => {
|
||||
match k {
|
||||
NamedKey::Enter => {
|
||||
debug!("Receiver enter");
|
||||
self.text.handle_enter();
|
||||
}
|
||||
NamedKey::Backspace => {
|
||||
// Clear dead key state if backspace is pressed
|
||||
if self.dead_key_state.is_some() {
|
||||
self.dead_key_state = None;
|
||||
debug!("Dead key state cleared");
|
||||
} else if self.text.backspace() {
|
||||
debug!("Backspace processed");
|
||||
}
|
||||
}
|
||||
NamedKey::Tab => {
|
||||
// Insert tab character
|
||||
self.text.insert_char('\t');
|
||||
}
|
||||
NamedKey::Escape => {
|
||||
// Clear any pending dead key state
|
||||
if self.dead_key_state.is_some() {
|
||||
self.dead_key_state = None;
|
||||
debug!("Dead key state cleared by Escape");
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowLeft => {
|
||||
if self.text.move_cursor(terminal_text::Motion::Left) {
|
||||
debug!("Cursor moved left");
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowRight => {
|
||||
if self.text.move_cursor(terminal_text::Motion::Right) {
|
||||
debug!("Cursor moved right");
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowUp => {
|
||||
if self.text.move_cursor(terminal_text::Motion::Up) {
|
||||
debug!("Cursor moved left");
|
||||
}
|
||||
}
|
||||
NamedKey::ArrowDown => {
|
||||
if self.text.move_cursor(terminal_text::Motion::Down) {
|
||||
debug!("Cursor moved right");
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
debug!("Unhandled named key: {:?}", k);
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Character(s) => {
|
||||
// Handle character input, considering dead keys
|
||||
if let Some(dead_char) = self.dead_key_state.take() {
|
||||
// Combine dead key with the current character if possible
|
||||
if let Some(combined) = self.combine_with_dead_key(dead_char, s) {
|
||||
for c in combined.chars() {
|
||||
debug!("Inserting character {c}");
|
||||
self.text.insert_char(c);
|
||||
}
|
||||
} else {
|
||||
// If combination not possible, insert both separately
|
||||
self.text.insert_char(dead_char);
|
||||
for c in s.chars() {
|
||||
debug!("Inserting character {c}");
|
||||
self.text.insert_char(c);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Normal character input
|
||||
for c in s.chars() {
|
||||
debug!("Inserting character {c}");
|
||||
self.text.insert_char(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
Key::Dead(dead_key) => {
|
||||
// Store dead key for later combination
|
||||
if let Some(c) = dead_key {
|
||||
self.dead_key_state = Some(*c);
|
||||
debug!("Dead key registered: {:?}", c);
|
||||
}
|
||||
}
|
||||
Key::Unidentified(unidentified) => {
|
||||
debug!(
|
||||
"Unidentified key: {:?}, location: {:?}",
|
||||
unidentified, event.physical_key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
self.window.request_redraw();
|
||||
}
|
||||
|
||||
pub fn handle_enter(&mut self) {
|
||||
self.text.handle_enter();
|
||||
self.window.request_redraw();
|
||||
/// Attempts to combine a dead key with a character
|
||||
fn combine_with_dead_key(&self, dead_key: char, input: &str) -> Option<String> {
|
||||
// This is a simplified implementation - for a complete solution,
|
||||
// you would need a more comprehensive mapping of dead key combinations
|
||||
|
||||
if input.chars().count() != 1 {
|
||||
return None; // Can only combine with single characters
|
||||
}
|
||||
|
||||
let base_char = input.chars().next().unwrap();
|
||||
|
||||
// Common dead key combinations
|
||||
match (dead_key, base_char) {
|
||||
// Acute accent (´)
|
||||
('´', 'a') => Some("á".to_string()),
|
||||
('´', 'e') => Some("é".to_string()),
|
||||
('´', 'i') => Some("í".to_string()),
|
||||
('´', 'o') => Some("ó".to_string()),
|
||||
('´', 'u') => Some("ú".to_string()),
|
||||
('´', 'A') => Some("Á".to_string()),
|
||||
('´', 'E') => Some("É".to_string()),
|
||||
('´', 'I') => Some("Í".to_string()),
|
||||
('´', 'O') => Some("Ó".to_string()),
|
||||
('´', 'U') => Some("Ú".to_string()),
|
||||
|
||||
// Grave accent (`)
|
||||
('`', 'a') => Some("à".to_string()),
|
||||
('`', 'e') => Some("è".to_string()),
|
||||
('`', 'i') => Some("ì".to_string()),
|
||||
('`', 'o') => Some("ò".to_string()),
|
||||
('`', 'u') => Some("ù".to_string()),
|
||||
('`', 'A') => Some("À".to_string()),
|
||||
('`', 'E') => Some("È".to_string()),
|
||||
('`', 'I') => Some("Ì".to_string()),
|
||||
('`', 'O') => Some("Ò".to_string()),
|
||||
('`', 'U') => Some("Ù".to_string()),
|
||||
|
||||
// Circumflex (^)
|
||||
('^', 'a') => Some("â".to_string()),
|
||||
('^', 'e') => Some("ê".to_string()),
|
||||
('^', 'i') => Some("î".to_string()),
|
||||
('^', 'o') => Some("ô".to_string()),
|
||||
('^', 'u') => Some("û".to_string()),
|
||||
('^', 'A') => Some("Â".to_string()),
|
||||
('^', 'E') => Some("Ê".to_string()),
|
||||
('^', 'I') => Some("Î".to_string()),
|
||||
('^', 'O') => Some("Ô".to_string()),
|
||||
('^', 'U') => Some("Û".to_string()),
|
||||
|
||||
// Diaeresis/umlaut (¨)
|
||||
('¨', 'a') => Some("ä".to_string()),
|
||||
('¨', 'e') => Some("ë".to_string()),
|
||||
('¨', 'i') => Some("ï".to_string()),
|
||||
('¨', 'o') => Some("ö".to_string()),
|
||||
('¨', 'u') => Some("ü".to_string()),
|
||||
('¨', 'A') => Some("Ä".to_string()),
|
||||
('¨', 'E') => Some("Ë".to_string()),
|
||||
('¨', 'I') => Some("Ï".to_string()),
|
||||
('¨', 'O') => Some("Ö".to_string()),
|
||||
('¨', 'U') => Some("Ü".to_string()),
|
||||
|
||||
// Tilde (~)
|
||||
('~', 'a') => Some("ã".to_string()),
|
||||
('~', 'n') => Some("ñ".to_string()),
|
||||
('~', 'o') => Some("õ".to_string()),
|
||||
('~', 'A') => Some("Ã".to_string()),
|
||||
('~', 'N') => Some("Ñ".to_string()),
|
||||
('~', 'O') => Some("Õ".to_string()),
|
||||
|
||||
// Cedilla (¸)
|
||||
('¸', 'c') => Some("ç".to_string()),
|
||||
('¸', 'C') => Some("Ç".to_string()),
|
||||
|
||||
// If no combination is defined, return None
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use glyphon::{
|
||||
Attrs, Buffer, BufferLine, Cache, Color, Cursor, Family, FontSystem, Metrics, Resolution,
|
||||
Shaping, SwashCache, TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
||||
Buffer, Cache, Color, Cursor, FontSystem, Metrics, Resolution, SwashCache, TextArea, TextAtlas,
|
||||
TextBounds, TextRenderer, Viewport, Wrap,
|
||||
};
|
||||
|
||||
pub use glyphon::cosmic_text::Motion;
|
||||
|
||||
use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat};
|
||||
|
||||
pub struct TerminalText {
|
||||
@@ -14,12 +16,28 @@ pub struct TerminalText {
|
||||
renderer: TextRenderer,
|
||||
dirty_regions: Vec<TextBounds>,
|
||||
cursor: Cursor,
|
||||
max_scroll_lines: usize,
|
||||
}
|
||||
|
||||
use log::info;
|
||||
// TODO
|
||||
// - need to update scroll when
|
||||
// - inserting into a line and it causes line wrap, creating a new visual line (currently the
|
||||
// scroll is only updated when creating a logical line or removing the last logical line)
|
||||
//
|
||||
// - need to keep track of the scroll vertical offset in two ways:
|
||||
// - first logical line displayed
|
||||
// - extra offset that covers possible wrapped lines before the first logical line
|
||||
//
|
||||
// - need to remove lines from the top of the buffer when the first logical line displayed is
|
||||
// bigger than max_scroll_offset
|
||||
|
||||
mod safe_casts;
|
||||
|
||||
use log::{debug, error, trace};
|
||||
|
||||
impl TerminalText {
|
||||
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText {
|
||||
debug!("Creating a new TerminalText object");
|
||||
let mut font_system = FontSystem::new();
|
||||
let swash_cache = SwashCache::new();
|
||||
|
||||
@@ -32,6 +50,7 @@ impl TerminalText {
|
||||
let mut buffer = Buffer::new(&mut font_system, Metrics::relative(13.0, 1.25));
|
||||
buffer.set_wrap(&mut font_system, Wrap::Glyph);
|
||||
|
||||
trace!("Loading system fonts");
|
||||
// Add a default font
|
||||
font_system.db_mut().load_system_fonts();
|
||||
|
||||
@@ -48,30 +67,122 @@ impl TerminalText {
|
||||
index: 0,
|
||||
affinity: glyphon::Affinity::After,
|
||||
},
|
||||
max_scroll_lines: 100,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
// Calculate the bounds delimited by the `start` and `end` lines
|
||||
fn get_text_bounds(&self, start: usize, end: usize) -> TextBounds {
|
||||
if start >= self.buffer.lines.len() || end > self.buffer.lines.len() {
|
||||
// Return default bounds for invalid line
|
||||
return TextBounds {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let line_height = self.buffer.metrics().line_height;
|
||||
let viewport_width = self.viewport.resolution().width;
|
||||
|
||||
let layout_iter = self.buffer.layout_runs();
|
||||
let mut top_line = 0;
|
||||
let mut bottom_line = 0;
|
||||
|
||||
let mut found_start_line = false;
|
||||
let mut found_end_line = false;
|
||||
|
||||
for (visual_line_count, run) in layout_iter.enumerate() {
|
||||
if run.line_i == start && !found_start_line {
|
||||
top_line = visual_line_count;
|
||||
found_start_line = true;
|
||||
}
|
||||
if run.line_i == end {
|
||||
if !found_end_line {
|
||||
found_end_line = true;
|
||||
}
|
||||
bottom_line = visual_line_count + 1;
|
||||
} else if found_end_line {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate bounds based on visual line positions
|
||||
let top_f32 = (safe_casts::usize_to_f32_or_max(top_line) * line_height).floor();
|
||||
let bottom_f32 = (safe_casts::usize_to_f32_or_max(bottom_line) * line_height).ceil();
|
||||
|
||||
// Safe conversions with overflow checks
|
||||
let top = safe_casts::f32_to_i32_or_bound(top_f32);
|
||||
let bottom = safe_casts::f32_to_i32_or_bound(bottom_f32);
|
||||
|
||||
// Ensure viewport width doesn't exceed i32::MAX
|
||||
let right = safe_casts::u32_to_i32_or_max(viewport_width.min(i32::MAX as u32));
|
||||
|
||||
TextBounds {
|
||||
left: 0,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
fn get_visible_line_range(&self) -> (usize, usize) {
|
||||
let viewport_height = safe_casts::u32_to_f32_or_max(self.viewport.resolution().height);
|
||||
let line_height = self.buffer.metrics().line_height;
|
||||
|
||||
let start_line = self.buffer.scroll().line;
|
||||
|
||||
trace!("Current start line: {start_line}");
|
||||
|
||||
// Calculate visible lines based on wrapped text
|
||||
let layout_iter = self.buffer.layout_runs();
|
||||
let mut last_logical_line = 0;
|
||||
|
||||
// Count how many visual lines we have and map to logical lines
|
||||
for (visual_line_count, run) in layout_iter.enumerate() {
|
||||
last_logical_line = run.line_i;
|
||||
|
||||
// If we've exceeded the viewport height, we can stop counting
|
||||
if (safe_casts::usize_to_f32_or_max(visual_line_count) * line_height) > viewport_height
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add 1 to include partially visible lines at the bottom
|
||||
let end_line = last_logical_line;
|
||||
|
||||
trace!("visible line range goes from {start_line} to {end_line}");
|
||||
|
||||
(start_line, end_line)
|
||||
}
|
||||
|
||||
fn ensure_visible_text_rendered(&mut self) {
|
||||
let (start_line, end_line) = self.get_visible_line_range();
|
||||
self.dirty_regions
|
||||
.push(self.get_text_bounds(start_line, end_line));
|
||||
}
|
||||
|
||||
fn merge_dirty_regions(&self) -> TextBounds {
|
||||
if self.dirty_regions.is_empty() {
|
||||
return TextBounds::default();
|
||||
}
|
||||
|
||||
self.dirty_regions.iter().fold(
|
||||
TextBounds {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
},
|
||||
|a, b| TextBounds {
|
||||
left: a.left.min(b.left),
|
||||
top: a.top.min(b.top),
|
||||
right: a.right.max(b.right),
|
||||
bottom: a.bottom.max(b.bottom),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) {
|
||||
@@ -84,192 +195,189 @@ impl TerminalText {
|
||||
},
|
||||
);
|
||||
|
||||
// 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();
|
||||
}
|
||||
let region = self.merge_dirty_regions();
|
||||
trace!("Preparing region {:?}", region);
|
||||
self.renderer
|
||||
.prepare(
|
||||
device,
|
||||
queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
&self.viewport,
|
||||
[TextArea {
|
||||
buffer: &self.buffer,
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
scale: 1.0,
|
||||
bounds: region,
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
self.dirty_regions.clear();
|
||||
}
|
||||
|
||||
pub fn render(&self, render_pass: &mut RenderPass) {
|
||||
pub fn render(&mut 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;
|
||||
fn with_update<F, R>(&mut self, operation: F) -> R
|
||||
where
|
||||
F: FnOnce(&mut Self) -> R,
|
||||
{
|
||||
let result = operation(self);
|
||||
|
||||
if line >= self.buffer.lines.len() {
|
||||
// TODO
|
||||
return;
|
||||
let mut scroll = self.buffer.scroll();
|
||||
if self.buffer.lines.len() > self.max_scroll_lines {
|
||||
self.buffer.lines.remove(0);
|
||||
self.cursor.line -= 1;
|
||||
}
|
||||
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;
|
||||
|
||||
scroll.line = self.buffer.lines.len();
|
||||
self.buffer.set_scroll(scroll);
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
self.ensure_visible_text_rendered();
|
||||
result
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
trace!("Resizing window - Width: {width} Height: {height}");
|
||||
|
||||
self.with_update(|this| {
|
||||
this.buffer.set_size(
|
||||
&mut this.font_system,
|
||||
Some(safe_casts::u32_to_f32_or_max(width)),
|
||||
Some(safe_casts::u32_to_f32_or_max(height)),
|
||||
);
|
||||
// Update the buffer's wrapping based on the new width
|
||||
this.buffer.set_wrap(&mut this.font_system, Wrap::Glyph)
|
||||
});
|
||||
}
|
||||
|
||||
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 insert_char(&mut self, c: char) {
|
||||
self.with_update(|this| {
|
||||
let line = this.cursor.line;
|
||||
|
||||
if line >= this.buffer.lines.len() {
|
||||
error!(
|
||||
"insert_char called when cursor is out of bounds! lines: {}, cursor: {}",
|
||||
this.buffer.lines.len(),
|
||||
line
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let mut text = this.buffer.lines[line].text().to_string().clone();
|
||||
let pos = this.cursor.index.min(text.len());
|
||||
trace!("Inserting char {c} in line {line}, position {pos}");
|
||||
text.insert(pos, c);
|
||||
|
||||
let ending = this.buffer.lines[line].ending();
|
||||
let attrs = this.buffer.lines[line].attrs_list().clone();
|
||||
|
||||
// Update the buffer with the new text
|
||||
this.buffer.lines[line].set_text(&text, ending, attrs);
|
||||
// Update cursor position
|
||||
this.cursor.index = pos + 1;
|
||||
})
|
||||
}
|
||||
|
||||
pub fn handle_enter(&mut self) {
|
||||
let line = self.cursor.line;
|
||||
self.with_update(|this| {
|
||||
let line = this.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;
|
||||
// Ensure line is within bounds
|
||||
if line >= this.buffer.lines.len() {
|
||||
error!(
|
||||
"handle_enter called when cursor is out of bounds! lines: {}, cursor: {}",
|
||||
this.buffer.lines.len(),
|
||||
line
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let pos = this.cursor.index.min(this.buffer.lines[line].text().len());
|
||||
trace!("Inserting newline in line {line}, position {pos}");
|
||||
let new_line = this.buffer.lines[line].split_off(pos);
|
||||
|
||||
// Update cursor position to beginning of next line
|
||||
this.cursor.line = line + 1;
|
||||
this.cursor.index = 0;
|
||||
// Create a new line with text after cursor
|
||||
this.buffer.lines.insert(this.cursor.line, new_line);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) -> bool {
|
||||
self.with_update(|this| {
|
||||
let line = this.cursor.line;
|
||||
|
||||
// Ensure line is within bounds
|
||||
if line >= this.buffer.lines.len() {
|
||||
error!(
|
||||
"handle_enter called when cursor is out of bounds! lines: {}, cursor: {}",
|
||||
this.buffer.lines.len(),
|
||||
line
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
let current_line = this.buffer.lines[line].text().to_string();
|
||||
let pos = this.cursor.index;
|
||||
|
||||
let mut ret = false;
|
||||
|
||||
if pos > 0 {
|
||||
// Delete character before cursor in current line
|
||||
let mut new_text = current_line.clone();
|
||||
new_text.remove(pos - 1);
|
||||
|
||||
// Update the line
|
||||
let ending = this.buffer.lines[line].ending();
|
||||
let attrs = this.buffer.lines[line].attrs_list().clone();
|
||||
this.buffer.lines[line].set_text(&new_text, ending, attrs);
|
||||
|
||||
// Move cursor back
|
||||
this.cursor.index = pos - 1;
|
||||
|
||||
ret = true;
|
||||
} else if line > 0 {
|
||||
// Move cursor to join point
|
||||
this.cursor.line = line - 1;
|
||||
this.cursor.index = this.buffer.lines[this.cursor.line].text().len();
|
||||
let current_line = this.buffer.lines[line].clone();
|
||||
// At beginning of line, join with previous line
|
||||
this.buffer.lines[this.cursor.line].append(current_line);
|
||||
// Remove current line
|
||||
this.buffer.lines.remove(line);
|
||||
|
||||
ret = true;
|
||||
}
|
||||
|
||||
ret
|
||||
})
|
||||
}
|
||||
|
||||
// Cursor movement methods
|
||||
pub fn move_cursor(&mut self, motion: Motion) -> bool {
|
||||
self.with_update(|this| {
|
||||
match this.buffer.cursor_motion(
|
||||
&mut this.font_system,
|
||||
this.cursor,
|
||||
Some(safe_casts::usize_to_i32_or_max(this.cursor.index)),
|
||||
motion,
|
||||
) {
|
||||
None => false,
|
||||
Some((cursor, _cursor_x_opt)) => {
|
||||
this.cursor = cursor;
|
||||
true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
69
src/app/rendering/terminal_text/safe_casts.rs
Normal file
69
src/app/rendering/terminal_text/safe_casts.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use log::trace;
|
||||
|
||||
// Safe conversions with overflow checks
|
||||
|
||||
pub fn u32_to_i32_or_max(n: u32) -> i32 {
|
||||
if n > i32::MAX as u32 {
|
||||
trace!(
|
||||
"Overflow casting {n}::u32 as i32, defaulting to {}",
|
||||
i32::MAX
|
||||
);
|
||||
i32::MAX
|
||||
} else {
|
||||
n as i32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn u32_to_f32_or_max(n: u32) -> f32 {
|
||||
if n > f32::MAX as u32 {
|
||||
trace!(
|
||||
"Overflow casting {n}::u32 as f32, defaulting to {}",
|
||||
f32::MAX
|
||||
);
|
||||
f32::MAX
|
||||
} else {
|
||||
n as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn usize_to_f32_or_max(n: usize) -> f32 {
|
||||
if n > f32::MAX as usize {
|
||||
trace!(
|
||||
"Overflow casting {n}::usize as f32, defaulting to {}",
|
||||
f32::MAX
|
||||
);
|
||||
f32::MAX
|
||||
} else {
|
||||
n as f32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn usize_to_i32_or_max(n: usize) -> i32 {
|
||||
if n > i32::MAX as usize {
|
||||
trace!(
|
||||
"Overflow casting {n}::usize as i32, defaulting to {}",
|
||||
i32::MAX
|
||||
);
|
||||
i32::MAX
|
||||
} else {
|
||||
n as i32
|
||||
}
|
||||
}
|
||||
|
||||
pub fn f32_to_i32_or_bound(n: f32) -> i32 {
|
||||
if n > i32::MAX as f32 {
|
||||
trace!(
|
||||
"Overflow casting {n}::f32 as i32, defaulting to {}",
|
||||
i32::MAX
|
||||
);
|
||||
i32::MAX
|
||||
} else if n < i32::MIN as f32 {
|
||||
trace!(
|
||||
"Underflow casting {n}::f32 as i32, defaulting to {}",
|
||||
i32::MIN
|
||||
);
|
||||
i32::MIN
|
||||
} else {
|
||||
n as i32
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user