extern crate sdl2; use clipboard::{ClipboardProvider, ClipboardContext}; use editor_render::GlyphAtlas; use num_format::{Locale, ToFormattedString}; use sdl2::{ event::{Event, WindowEvent}, keyboard::Keycode, pixels::Color, rect::{Point, Rect}, render::Canvas, video::Window, }; mod editor_render; static SCREEN_WIDTH: u32 = 1280; static SCREEN_HEIGHT: u32 = 720; // TODO: Make this configurable static REFRESH_RATE: u32 = 50; // TODO: Make this configurable static UNDO_TIME: u32 = 250; static UNDO_TIME_COUNT: u32 = (REFRESH_RATE as f32 * (UNDO_TIME as f32 / 1000.0)) as u32; struct ModifierKeys { alt: bool, ctrl: bool, shift: bool, } // struct EditorGraphics { // canvas: Canvas, // window_size: (u32, u32), // glyph_atlas: GlyphAtlas, // } /// Draw all contents to the window fn draw(canvas: &mut Canvas, window_size: (u32, u32), glyph_atlas: &GlyphAtlas, buffer: &str, cursor_position: usize) -> Result<(), String> { let text_offset = Point::new(10, 10); // Draw background canvas.set_draw_color(Color::RGB(32, 32, 32)); canvas.clear(); // Draw text canvas.set_draw_color(Color::RGB(240, 240, 240)); let fb_text = editor_render::draw_text( &glyph_atlas.glyphs, &glyph_atlas.metrics, buffer, text_offset ); canvas.draw_points(&fb_text[..])?; // Draw info let status = buffer.len().to_formatted_string(&Locale::en); let status_position = Point::new( text_offset.x, window_size.1 as i32 - glyph_atlas.metrics.height as i32 * 2 ); canvas.set_draw_color(Color::RGB(16, 64, 64)); canvas.fill_rect(Rect::new(0, status_position.y - 5, window_size.0, glyph_atlas.metrics.height as u32 + 10 ))?; canvas.set_draw_color(Color::RGB(127, 240, 240)); let status_bar = editor_render::draw_text( &glyph_atlas.glyphs, &glyph_atlas.metrics, &status, status_position ); canvas.draw_points(&status_bar[..])?; // Draw cursor canvas.set_draw_color(Color::RGB(64, 240, 240)); let fb_cursor = editor_render::draw_cursor( &glyph_atlas.metrics, cursor_position, buffer, text_offset ); canvas.draw_line(fb_cursor.0, fb_cursor.1)?; canvas.present(); Ok(()) } pub fn main() -> Result<(), String> { // Initialize clipboard let mut clipboard_context: ClipboardContext = ClipboardProvider::new().unwrap(); // Initialize SDL2, window, and canvas let sdl_context = sdl2::init()?; let video_subsys = sdl_context.video()?; let window = video_subsys .window("Rude", SCREEN_WIDTH, SCREEN_HEIGHT) .position_centered() .resizable() .opengl() .build() .map_err(|e| e.to_string())?; let mut canvas = window.into_canvas().build().map_err(|e| e.to_string())?; let mut window_size = canvas.output_size()?; // Initalize buffer let mut buffer = String::new(); // Initialize undo data & values let mut undo_history: Vec<(String, usize)> = vec![]; let mut undo_position: usize = 0; let mut undo_timer: u32 = UNDO_TIME_COUNT; // Initialize input values let mut modifier_keys = ModifierKeys {alt: false, ctrl: false, shift: false}; let mut cursor_position = 0; let mut selection_anchor: Option = None; // Initialize graphics data and values let glyph_atlas = editor_render::generate_glyph_data(); // Easier way to please the borrow checker macro_rules! draw { () => { draw(&mut canvas, window_size, &glyph_atlas, &buffer, cursor_position)? }; } draw!(); 'mainloop: loop { // TODO: Make this completely user-configurable instead of hardcoded for event in sdl_context.event_pump()?.poll_iter() { match event { Event::Window { win_event, .. } => { if let WindowEvent::Resized(w, h) = win_event { window_size = (w as u32, h as u32); draw!(); } } // ESC or SIGKILL // Event::KeyDown { keycode: Some(Keycode::Escape), .. } | Event::Quit { .. } => break 'mainloop, Event::KeyUp { keycode, .. } => { match keycode{ Some(Keycode::LAlt) | Some(Keycode::RAlt) => { modifier_keys.alt = false }, Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => { modifier_keys.ctrl = false }, Some(Keycode::LShift) | Some(Keycode::RShift) => { modifier_keys.shift = false }, Some(Keycode::LGui) | Some(Keycode::RGui) => { break }, _ => (), } }, Event::KeyDown { keycode, .. } => { match keycode { Some(Keycode::LAlt) | Some(Keycode::RAlt) => { modifier_keys.alt = true }, Some(Keycode::LCtrl) | Some(Keycode::RCtrl) => { modifier_keys.ctrl = true }, Some(Keycode::LShift) | Some(Keycode::RShift) => { modifier_keys.shift = true }, Some(Keycode::LGui) | Some(Keycode::RGui) => { break }, _ => (), }; match (modifier_keys.shift, modifier_keys.ctrl, modifier_keys.alt) { // All modifiers up (false, false, false) => { match keycode { // DELETE key Some(Keycode::Delete) => { if buffer.len() > 0 && cursor_position < buffer.len() { undo_timer = 0; selection_anchor = None; buffer.remove(cursor_position); draw!(); } }, // ENTER key Some(Keycode::Return) => { undo_timer = 0; selection_anchor = None; let key = '\n'; buffer.insert(cursor_position, key); cursor_position += 1; draw!(); }, // HOME key Some(Keycode::Home) => { selection_anchor = None; cursor_position = 0; draw!(); }, // END key Some(Keycode::End) => { selection_anchor = None; cursor_position = buffer.len(); draw!(); }, // Left/Back arrow Some(Keycode::Left) => { selection_anchor = None; cursor_position = usize::checked_sub(cursor_position, 1) .unwrap_or(0); draw!(); }, // Right/Forward arrow Some(Keycode::Right) => { selection_anchor = None; cursor_position = (cursor_position + 1).min(buffer.len()); draw!(); }, // BACKSPACE key // Character backspace Some(Keycode::Backspace) => { if buffer.len() > 0 { undo_timer = 0; selection_anchor = None; buffer.remove(cursor_position - 1); cursor_position -= 1; draw!(); } }, _ => (), } }, // CTRL down (false, true, false) => { match keycode { // Select all Some(Keycode::A) => { selection_anchor = Some(buffer.len()); cursor_position = 0; draw!() }, // Undo Some(Keycode::Z) => { if undo_position > 1 { undo_position -= 1; let last_undo = undo_history[undo_position - 1].clone(); buffer = last_undo.0; cursor_position = last_undo.1; draw!() } }, // TODO: Cut Some(Keycode::X) => println!("Cut"), // Copy // TODO: Use selection Some(Keycode::C) => { clipboard_context.set_contents(buffer.clone()).unwrap() }, // Paste Some(Keycode::V) => { let paste = clipboard_context.get_contents().unwrap(); for character in paste.chars() { buffer.insert(cursor_position, character); cursor_position += 1; undo_timer = UNDO_TIME_COUNT; } draw!() }, // BACKSPACE key // Word backspace // TODO: Clean up this cursed expression Some(Keycode::Backspace) => { if buffer.len() > 0 { undo_timer = 0; selection_anchor = None; let buffer_chars: Vec = buffer.chars() .collect(); while !(buffer_chars[cursor_position - 1] == ' ' || buffer_chars[cursor_position - 1] == '\n') && cursor_position > 1 { buffer.remove(cursor_position - 1); cursor_position -= 1; } buffer.remove(cursor_position - 1); cursor_position -= 1; draw!() } }, _ => (), } }, // SHIFT + CTRL down (true, true, false) => { match keycode { Some(Keycode::Z) => { if undo_position < undo_history.len() { undo_position += 1; let last_redo = undo_history[undo_position - 1].clone(); buffer = last_redo.0; cursor_position = last_redo.1; draw!(); } }, Some(Keycode::X) => println!("Cut line(s)"), Some(Keycode::C) => println!("Copy line(s)"), _ => (), } }, _ => (), } }, // Process user input Event::TextInput { text, .. } => { undo_timer = 0; selection_anchor = None; let input_char = text.chars().nth(0).expect("Empty"); buffer.insert(cursor_position, input_char); cursor_position += 1; draw!(); }, _ => {} } } // Wait for pause in input to snapshot buffer for undoing/redoing if undo_timer < UNDO_TIME_COUNT { undo_timer += 1; } else if undo_timer == UNDO_TIME_COUNT { // Upon editing after an undo, this clears every action after such undo if undo_position < undo_history.len() { undo_history.truncate(undo_position); } undo_history.push((buffer.clone(), cursor_position)); undo_timer += 1; undo_position += 1; } ::std::thread::sleep(std::time::Duration::new(0, 1_000_000_000 / REFRESH_RATE)); } println!("{buffer}"); Ok(()) }