| use std::sync::Arc; |
| use std::time::Instant; |
|
|
| use log::info; |
| use winit::application::ApplicationHandler; |
| use winit::event::WindowEvent; |
| use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; |
| use winit::window::{Window, WindowId}; |
|
|
| use spectral_core::{Config, Terminal}; |
| use spectral_font::{Font, FontFamily, FontStyle, GlyphKey}; |
| use spectral_render::Renderer; |
|
|
| struct SpectralApp { |
| window: Option<Arc<Window>>, |
| renderer: Option<Renderer>, |
| terminal: Terminal, |
| config: Config, |
| font_family: Option<FontFamily>, |
| last_render: Instant, |
| } |
|
|
| impl SpectralApp { |
| fn new(config: Config) -> Self { |
| let terminal = Terminal::new(config.clone()); |
| Self { |
| window: None, |
| renderer: None, |
| terminal, |
| config, |
| font_family: None, |
| last_render: Instant::now(), |
| } |
| } |
|
|
| fn init_window(&mut self, event_loop: &ActiveEventLoop) { |
| let window_attrs = Window::default_attributes() |
| .with_title("Spectral Terminal") |
| .with_inner_size(winit::dpi::LogicalSize::new(800, 600)); |
| let window = Arc::new( |
| event_loop.create_window(window_attrs).expect("Failed to create window"), |
| ); |
| let rt = tokio::runtime::Runtime::new().unwrap(); |
| let renderer = rt.block_on(Renderer::new( |
| window.clone(), |
| self.terminal.grid.cols as u32, |
| self.terminal.grid.rows as u32, |
| 2048, |
| )); |
| self.window = Some(window); |
| self.renderer = Some(renderer); |
| self.load_fonts(); |
| } |
|
|
| fn load_fonts(&mut self) { |
| let font_names = [ |
| &*self.config.font_family, |
| "Iosevka Extended", "Iosevka", "JetBrains Mono", "Fira Code", "monospace", |
| ]; |
| for &name in &font_names { |
| if let Some(font_data) = try_load_font(name) { |
| let mut family = FontFamily::new(name); |
| if let Some(font) = Font::from_bytes(0, font_data, 0, FontStyle::Regular) { |
| family.add_font(font); |
| self.font_family = Some(family); |
| info!("Loaded font: {}", name); |
| break; |
| } |
| } |
| } |
| if let Some(renderer) = &mut self.renderer { |
| if let Some(family) = &self.font_family { |
| if let Some(font) = family.get_font(FontStyle::Regular) { |
| let px = self.config.font_size; |
| for ch in (32u8..=126).map(|b| b as char) { |
| let glyph_id = font.lookup_glyph(ch); |
| let (metrics, bitmap) = font.rasterize_glyph(glyph_id, px); |
| let w = metrics.width as u16; |
| let h = metrics.height as u16; |
| if w > 0 && h > 0 { |
| let key = GlyphKey { |
| font_id: font.id, glyph_id, |
| size_bucket: (px * 4.0) as u16, |
| style_flags: 0, |
| }; |
| let rgba = spectral_font::msdf::r8_to_rgba(&bitmap); |
| renderer.glyph_atlas.insert(key, w, h, &rgba); |
| } |
| } |
| renderer.needs_atlas_upload = true; |
| } |
| } |
| } |
| } |
|
|
| fn handle_input(&mut self, text: &str) { |
| let bytes = text.as_bytes(); |
| self.terminal.feed(bytes); |
| self.request_redraw(); |
| } |
|
|
| fn request_redraw(&mut self) { |
| if let Some(window) = &self.window { |
| window.request_redraw(); |
| } |
| } |
|
|
| fn render(&mut self) { |
| if let Some(renderer) = &mut self.renderer { |
| renderer.update_atlas(); |
| renderer.update_instances(&self.terminal); |
| renderer.render(); |
| } |
| self.last_render = Instant::now(); |
| } |
| } |
|
|
| fn try_load_font(name: &str) -> Option<Arc<Vec<u8>>> { |
| let paths = [ |
| format!("/usr/share/fonts/truetype/{}/{}.ttf", name.to_lowercase().replace(' ', ""), name), |
| format!("/usr/share/fonts/TTF/{}.ttf", name.replace(' ', "")), |
| format!("/usr/share/fonts/truetype/{}.ttf", name.replace(' ', "")), |
| format!("/usr/share/fonts/{}.ttf", name), |
| format!("/usr/share/fonts/truetype/{}.ttf", name.to_lowercase()), |
| format!("/usr/share/fonts/truetype/iosevka/{}-Regular.ttf", name.replace(' ', "")), |
| format!("/usr/share/fonts/truetype/iosevka/{}-Regular.ttf", name.replace(" Extended", "")), |
| ]; |
| for path in &paths { |
| if let Ok(data) = std::fs::read(path) { |
| return Some(Arc::new(data)); |
| } |
| } |
| if let Ok(output) = std::process::Command::new("fc-match") |
| .args(["-f", "%{file}", name]).output() |
| { |
| let path = String::from_utf8_lossy(&output.stdout); |
| let path = path.trim(); |
| if !path.is_empty() && path != name { |
| if let Ok(data) = std::fs::read(path) { |
| return Some(Arc::new(data)); |
| } |
| } |
| } |
| None |
| } |
|
|
| impl ApplicationHandler for SpectralApp { |
| fn resumed(&mut self, event_loop: &ActiveEventLoop) { |
| if self.window.is_none() { self.init_window(event_loop); } |
| } |
|
|
| fn window_event(&mut self, event_loop: &ActiveEventLoop, _window_id: WindowId, event: WindowEvent) { |
| match event { |
| WindowEvent::CloseRequested => event_loop.exit(), |
| WindowEvent::Resized(size) => { |
| if let Some(renderer) = &mut self.renderer { |
| renderer.resize(size.width, size.height); |
| } |
| } |
| WindowEvent::KeyboardInput { event, .. } => { |
| if event.state == winit::event::ElementState::Pressed { |
| if let Some(text) = event.text { |
| self.handle_input(&text); |
| } |
| } |
| } |
| WindowEvent::RedrawRequested => { self.render(); } |
| _ => {} |
| } |
| } |
|
|
| fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {} |
| } |
|
|
| fn main() { |
| env_logger::init(); |
| let event_loop = EventLoop::new().unwrap(); |
| event_loop.set_control_flow(ControlFlow::Poll); |
| let config = Config::default(); |
| let mut app = SpectralApp::new(config); |
| event_loop.run_app(&mut app).unwrap(); |
| } |
|
|