Compare commits
4 Commits
main
...
ae56f78075
| Author | SHA1 | Date | |
|---|---|---|---|
| ae56f78075 | |||
| b0ecc5623c | |||
| 3715ecf841 | |||
| 1ec12fa728 |
23
.gitignore
vendored
23
.gitignore
vendored
@@ -1,22 +1 @@
|
||||
# ---> Rust
|
||||
# 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/
|
||||
/target
|
||||
|
||||
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"
|
||||
69
src/app/mod.rs
Normal file
69
src/app/mod.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::runtime::Runtime;
|
||||
use winit::dpi::LogicalSize;
|
||||
use winit::event::WindowEvent;
|
||||
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() {
|
||||
state.handle_keyboard_input(&event);
|
||||
}
|
||||
}
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
215
src/app/rendering/mod.rs
Normal file
215
src/app/rendering/mod.rs
Normal file
@@ -0,0 +1,215 @@
|
||||
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::event::KeyEvent;
|
||||
use winit::keyboard::{Key, NamedKey};
|
||||
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.text.resize(new_size.width, new_size.height);
|
||||
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 handle_keyboard_input(&mut self, event: &KeyEvent) {
|
||||
match &event.logical_key {
|
||||
Key::Named(k) => match k {
|
||||
NamedKey::Enter => {
|
||||
self.text.handle_enter();
|
||||
}
|
||||
_ => todo!(),
|
||||
},
|
||||
Key::Character(s) => {
|
||||
for c in s.chars() {
|
||||
self.text.insert_char(c);
|
||||
}
|
||||
}
|
||||
Key::Dead(_) => {
|
||||
todo!()
|
||||
}
|
||||
Key::Unidentified(_) => {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
self.window.request_redraw();
|
||||
}
|
||||
}
|
||||
346
src/app/rendering/terminal_text/mod.rs
Normal file
346
src/app/rendering/terminal_text/mod.rs
Normal file
@@ -0,0 +1,346 @@
|
||||
use glyphon::{
|
||||
Buffer, BufferLine, Cache, Color, Cursor, FontSystem, Metrics, Resolution, Shaping, SwashCache,
|
||||
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
||||
};
|
||||
|
||||
use wgpu::{Device, MultisampleState, Queue, RenderPass, SurfaceConfiguration, TextureFormat};
|
||||
|
||||
pub struct TerminalText {
|
||||
font_system: FontSystem,
|
||||
cache: SwashCache,
|
||||
viewport: Viewport,
|
||||
atlas: TextAtlas,
|
||||
buffer: Buffer,
|
||||
renderer: TextRenderer,
|
||||
dirty_regions: Vec<TextBounds>,
|
||||
cursor: Cursor,
|
||||
}
|
||||
|
||||
use log::{debug, trace};
|
||||
|
||||
impl TerminalText {
|
||||
pub fn new(device: &Device, queue: &Queue, surface_format: TextureFormat) -> TerminalText {
|
||||
debug!("Creating a new TerminalText object");
|
||||
let mut font_system = FontSystem::new();
|
||||
let swash_cache = SwashCache::new();
|
||||
|
||||
let cache = Cache::new(device);
|
||||
let viewport = Viewport::new(device, &cache);
|
||||
let mut atlas = TextAtlas::new(device, queue, &cache, surface_format);
|
||||
|
||||
let renderer = TextRenderer::new(&mut atlas, device, MultisampleState::default(), None);
|
||||
|
||||
let mut buffer = Buffer::new(&mut font_system, Metrics::relative(13.0, 1.25));
|
||||
buffer.set_wrap(&mut font_system, Wrap::Glyph);
|
||||
|
||||
trace!("Loading system fonts");
|
||||
// Add a default font
|
||||
font_system.db_mut().load_system_fonts();
|
||||
|
||||
Self {
|
||||
font_system,
|
||||
cache: swash_cache,
|
||||
viewport,
|
||||
atlas,
|
||||
buffer,
|
||||
renderer,
|
||||
dirty_regions: vec![],
|
||||
cursor: Cursor {
|
||||
line: 0,
|
||||
index: 0,
|
||||
affinity: glyphon::Affinity::After,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: u32, height: u32) {
|
||||
trace!("Resizing window - Width: {width} Height: {height}");
|
||||
self.buffer.set_size(
|
||||
&mut self.font_system,
|
||||
Some(width as f32),
|
||||
Some(height as f32),
|
||||
);
|
||||
// Update the buffer's wrapping based on the new width
|
||||
self.buffer.set_wrap(&mut self.font_system, Wrap::Glyph);
|
||||
// Reshape the text with the new dimensions
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
self.ensure_visible_text_rendered();
|
||||
}
|
||||
|
||||
fn merge_dirty_regions(&self) -> TextBounds {
|
||||
TextBounds {
|
||||
left: self
|
||||
.dirty_regions
|
||||
.iter()
|
||||
.min_by_key(|b| b.left)
|
||||
.unwrap()
|
||||
.left,
|
||||
top: self.dirty_regions.iter().min_by_key(|b| b.top).unwrap().top,
|
||||
right: self
|
||||
.dirty_regions
|
||||
.iter()
|
||||
.max_by_key(|b| b.right)
|
||||
.unwrap()
|
||||
.right,
|
||||
bottom: self
|
||||
.dirty_regions
|
||||
.iter()
|
||||
.max_by_key(|b| b.bottom)
|
||||
.unwrap()
|
||||
.bottom,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self, device: &Device, queue: &Queue, config: &SurfaceConfiguration) {
|
||||
// Update viewport with new dimensions
|
||||
self.viewport.update(
|
||||
queue,
|
||||
Resolution {
|
||||
width: config.width,
|
||||
height: config.height,
|
||||
},
|
||||
);
|
||||
|
||||
if !self.dirty_regions.is_empty() {
|
||||
let region = self.merge_dirty_regions();
|
||||
trace!("Preparing region {:?}", region);
|
||||
self.renderer
|
||||
.prepare(
|
||||
device,
|
||||
queue,
|
||||
&mut self.font_system,
|
||||
&mut self.atlas,
|
||||
&self.viewport,
|
||||
[TextArea {
|
||||
buffer: &self.buffer,
|
||||
left: 0.0,
|
||||
top: 0.0,
|
||||
scale: 1.0,
|
||||
bounds: region,
|
||||
default_color: Color::rgb(255, 255, 255),
|
||||
custom_glyphs: &[],
|
||||
}],
|
||||
&mut self.cache,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
self.dirty_regions.clear();
|
||||
}
|
||||
|
||||
pub fn render(&self, render_pass: &mut RenderPass) {
|
||||
self.renderer
|
||||
.render(&self.atlas, &self.viewport, render_pass)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn trim(&mut self) {
|
||||
self.atlas.trim()
|
||||
}
|
||||
|
||||
pub fn insert_char(&mut self, c: char) {
|
||||
let line = self.cursor.line;
|
||||
|
||||
if line >= self.buffer.lines.len() {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
|
||||
let mut text = self.buffer.lines[line].text().to_string().clone();
|
||||
let pos = self.cursor.index.min(text.len());
|
||||
text.insert(pos, c);
|
||||
|
||||
let ending = self.buffer.lines[line].ending();
|
||||
let attrs = self.buffer.lines[line].attrs_list().clone();
|
||||
|
||||
// Update the buffer with the new text
|
||||
self.buffer.lines[line].set_text(&text, ending, attrs);
|
||||
// Update cursor position
|
||||
self.cursor.index = pos + 1;
|
||||
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
self.ensure_visible_text_rendered();
|
||||
}
|
||||
|
||||
pub fn handle_enter(&mut self) {
|
||||
let line = self.cursor.line;
|
||||
|
||||
// Ensure line is within bounds
|
||||
if line >= self.buffer.lines.len() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the current line text
|
||||
let current_line = self.buffer.lines[line].text().to_string();
|
||||
let pos = self.cursor.index.min(current_line.len());
|
||||
|
||||
// Split the line at cursor position
|
||||
let text_before = ¤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);
|
||||
self.buffer.lines.insert(line + 1, new_line);
|
||||
|
||||
// Update cursor position to beginning of next line
|
||||
self.cursor.line = line + 1;
|
||||
self.cursor.index = 0;
|
||||
|
||||
// Reshape the buffer
|
||||
self.buffer.shape_until_scroll(&mut self.font_system, false);
|
||||
self.ensure_visible_text_rendered();
|
||||
}
|
||||
|
||||
fn get_line_bounds(&self, line: usize) -> TextBounds {
|
||||
// Calculate the bounds for a specific line
|
||||
if line >= 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;
|
||||
|
||||
// Use layout_runs to get information about wrapped lines
|
||||
let layout_iter = self.buffer.layout_runs();
|
||||
let mut top_line = 0;
|
||||
let mut bottom_line = 0;
|
||||
let mut found_line = false;
|
||||
|
||||
// Iterate through layout runs to find our target line
|
||||
for (visual_line_count, run) in layout_iter.enumerate() {
|
||||
if run.line_i == line {
|
||||
// This run belongs to our target line
|
||||
if !found_line {
|
||||
// First run of our target line
|
||||
top_line = visual_line_count;
|
||||
found_line = true;
|
||||
}
|
||||
// Update bottom line for each run we find for this line
|
||||
bottom_line = visual_line_count + 1;
|
||||
} else if found_line {
|
||||
// We've processed all runs for our target line
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate bounds based on visual line positions
|
||||
let top_f32 = (top_line as f32 * line_height).floor();
|
||||
let bottom_f32 = (bottom_line as f32 * line_height).ceil();
|
||||
|
||||
// Safe conversions with overflow checks
|
||||
let top = if top_f32 > i32::MAX as f32 {
|
||||
i32::MAX
|
||||
} else if top_f32 < i32::MIN as f32 {
|
||||
i32::MIN
|
||||
} else {
|
||||
top_f32 as i32
|
||||
};
|
||||
|
||||
let bottom = if bottom_f32 > i32::MAX as f32 {
|
||||
i32::MAX
|
||||
} else if bottom_f32 < i32::MIN as f32 {
|
||||
i32::MIN
|
||||
} else {
|
||||
bottom_f32 as i32
|
||||
};
|
||||
|
||||
// Ensure viewport width doesn't exceed i32::MAX
|
||||
let right = viewport_width.min(i32::MAX as u32) as i32;
|
||||
|
||||
TextBounds {
|
||||
left: 0,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_visible_line_range(&self) -> (usize, usize) {
|
||||
let viewport_height = self.viewport.resolution().height as f32;
|
||||
let line_height = self.buffer.metrics().line_height;
|
||||
|
||||
// Start from line 0 (no scrolling yet)
|
||||
let start_line = 0;
|
||||
|
||||
// Calculate 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 ((start_line + visual_line_count) as f32 * line_height) > viewport_height {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Add 1 to include partially visible lines at the bottom
|
||||
let end_line = if self.buffer.lines.is_empty() {
|
||||
0
|
||||
} else {
|
||||
// Make sure we include at least the last logical line that has content in the viewport
|
||||
(last_logical_line + 1).min(self.buffer.lines.len())
|
||||
};
|
||||
|
||||
trace!("visible line range goes from {start_line} to {end_line}");
|
||||
|
||||
(start_line, end_line)
|
||||
}
|
||||
|
||||
pub fn ensure_visible_text_rendered(&mut self) {
|
||||
let (start_line, end_line) = self.get_visible_line_range();
|
||||
for i in start_line..end_line {
|
||||
self.dirty_regions.push(self.get_line_bounds(i));
|
||||
}
|
||||
}
|
||||
|
||||
// pub fn mark_all_dirty(&mut self) {
|
||||
// trace!("Marking all regions as dirty");
|
||||
// // Create a single region covering the entire viewport
|
||||
// let full_bounds = TextBounds {
|
||||
// left: 0,
|
||||
// top: 0,
|
||||
// right: self.viewport.resolution().width as i32,
|
||||
// bottom: self.viewport.resolution().height as i32,
|
||||
// };
|
||||
// self.dirty_regions = vec![full_bounds];
|
||||
// }
|
||||
|
||||
// pub fn move_cursor_left(&mut self) {
|
||||
// if self.cursor.index > 0 {
|
||||
// self.cursor.index -= 1;
|
||||
// } else if self.cursor.line > 0 {
|
||||
// // Move to end of previous line
|
||||
// self.cursor.line -= 1;
|
||||
// let prev_line_len = self.buffer.lines[self.cursor.line].text().len();
|
||||
// self.cursor.index = prev_line_len;
|
||||
// }
|
||||
// }
|
||||
|
||||
// pub fn move_cursor_right(&mut self) {
|
||||
// if self.cursor.line < self.buffer.lines.len() {
|
||||
// let current_line_len = self.buffer.lines[self.cursor.line].text().len();
|
||||
|
||||
// if self.cursor.index < current_line_len {
|
||||
// self.cursor.index += 1;
|
||||
// } else if self.cursor.line < self.buffer.lines.len() - 1 {
|
||||
// // Move to beginning of next line
|
||||
// self.cursor.line += 1;
|
||||
// self.cursor.index = 0;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
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