feat: first commit
This commit is contained in:
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" }
|
||||
wgpu = "25"
|
||||
glyphon = "0.9"
|
||||
|
||||
termion = "4"
|
||||
|
||||
env_logger = "0.10"
|
||||
log = "0.4"
|
||||
67
src/app/mod.rs
Normal file
67
src/app/mod.rs
Normal file
@@ -0,0 +1,67 @@
|
||||
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,
|
||||
} => {
|
||||
todo!()
|
||||
}
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
187
src/app/rendering/mod.rs
Normal file
187
src/app/rendering/mod.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use std::sync::Arc;
|
||||
use wgpu::{CompositeAlphaMode, MultisampleState, TextureFormat};
|
||||
use winit::window::Window;
|
||||
|
||||
mod terminal_text;
|
||||
|
||||
pub struct State {
|
||||
surface: wgpu::Surface<'static>,
|
||||
device: wgpu::Device,
|
||||
queue: wgpu::Queue,
|
||||
config: wgpu::SurfaceConfiguration,
|
||||
|
||||
size: winit::dpi::PhysicalSize<u32>,
|
||||
|
||||
text: terminal_text::TerminalText,
|
||||
|
||||
window: Arc<Window>,
|
||||
}
|
||||
|
||||
impl State {
|
||||
pub fn resize(&mut self, new_size: winit::dpi::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<(), wgpu::SurfaceError> {
|
||||
// Use a longer text string to better demonstrate wrapping
|
||||
self.text.set_text("This is a long text string that should wrap when the window is resized. The terminal emulator should handle text wrapping properly to ensure that all text is visible and properly formatted regardless of the window dimensions. This demonstrates the text wrapping functionality using glyphon library.\n\nMultiple paragraphs should also work correctly with proper wrapping behavior.");
|
||||
|
||||
// Prepare with current configuration (which includes the current window size)
|
||||
self.text.prepare(&self.device, &self.queue, &self.config);
|
||||
|
||||
let output = self.surface.get_current_texture()?;
|
||||
let view = output
|
||||
.texture
|
||||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||||
let mut encoder = self
|
||||
.device
|
||||
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Render Encoder"),
|
||||
});
|
||||
|
||||
{
|
||||
let mut _render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("Render Pass"),
|
||||
color_attachments: &[Some(wgpu::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();
|
||||
let scale_factor = window.scale_factor();
|
||||
|
||||
// The instance is needed to create an `Adapter` and a `Surface`
|
||||
// Backends::all => Vulkan + Metal + DX12 + Browser WebGPU
|
||||
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
|
||||
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(&wgpu::RequestAdapterOptions {
|
||||
power_preference: wgpu::PowerPreference::default(),
|
||||
compatible_surface: Some(&surface),
|
||||
force_fallback_adapter: false,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let (device, queue) = adapter
|
||||
.request_device(&wgpu::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 = wgpu::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,
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/app/rendering/terminal_text/mod.rs
Normal file
120
src/app/rendering/terminal_text/mod.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use glyphon::{
|
||||
Attrs, Buffer, Cache, Color, Family, FontSystem, Metrics, Resolution, Shaping, SwashCache,
|
||||
TextArea, TextAtlas, TextBounds, TextRenderer, Viewport, Wrap,
|
||||
};
|
||||
|
||||
use wgpu::{Device, MultisampleState, Queue, RenderPass, TextureFormat};
|
||||
|
||||
pub struct TerminalText {
|
||||
font_system: FontSystem,
|
||||
cache: SwashCache,
|
||||
viewport: Viewport,
|
||||
atlas: TextAtlas,
|
||||
buffer: Buffer,
|
||||
renderer: TextRenderer,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
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: &wgpu::Device,
|
||||
queue: &wgpu::Queue,
|
||||
config: &wgpu::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);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
8
src/main.rs
Normal file
8
src/main.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
use winit::event_loop::EventLoop;
|
||||
|
||||
mod app;
|
||||
|
||||
fn main() {
|
||||
let event_loop = EventLoop::new().unwrap();
|
||||
event_loop.run_app(&mut app::Application::new()).unwrap();
|
||||
}
|
||||
Reference in New Issue
Block a user