From 7615546fb0157c3ec9d2f25ec9837ee0b6cb7e9a Mon Sep 17 00:00:00 2001 From: Akshay Date: Wed, 17 Mar 2021 17:52:40 +0530 Subject: feat: basic command mode, add text box primitives --- src/app.rs | 308 +++++++++++++++++++++++++++++++++++++++------------------ src/command.rs | 134 +++++++++++++++++++++++++ src/main.rs | 36 ++++++- src/utils.rs | 17 +++- 4 files changed, 396 insertions(+), 99 deletions(-) create mode 100644 src/command.rs (limited to 'src') diff --git a/src/app.rs b/src/app.rs index 5b27422..0eac084 100644 --- a/src/app.rs +++ b/src/app.rs @@ -23,21 +23,29 @@ use sdl2::{ Sdl, }; +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Mode { + Draw, + Command, +} + pub struct AppState<'ctx> { active_color: bool, brush_size: u8, - dither_level: u8, canvas: Canvas, context: &'ctx Sdl, - ttf_context: &'ctx Sdl2TtfContext, - mouse: (i32, i32), current_operation: Operation, + dither_level: u8, grid: Grid, - last_point: Option, + last_point: Option, + mode: Mode, + mouse: (i32, i32), pixmap: Pixmap, start: Point, symmetry: Symmetry, + ttf_context: &'ctx Sdl2TtfContext, undo_stack: UndoStack, + command_box: CommandBox, zoom: u8, } @@ -153,15 +161,15 @@ impl<'ctx> AppState<'ctx> { fn paint_line>( &mut self, - start: P, + start: MapPoint, end: P, val: bool, ) -> Result, ()> { - let start = self.idx_at_coord(start).ok_or(())?; + let MapPoint { x, y } = start; let end = self.idx_at_coord(end).ok_or(())?; let dither_level = self.dither_level; - let line = self.pixmap.get_line(start, end); + let line = self.pixmap.get_line((x, y), end); let sym_line = self.symmetry.apply(&line); let mut line_modify_record = vec![]; @@ -239,6 +247,21 @@ impl<'ctx> AppState<'ctx> { } } + fn eval_command(&mut self) { + match self.command_box.text.as_str() { + "(save)" => { + let image = self.export(); + let encoded = image.encode().unwrap(); + let mut buffer = File::create("test.obi").unwrap(); + eprintln!("writing to file"); + buffer.write_all(&encoded[..]).unwrap(); + } + _ => {} + } + self.command_box.clear(); + self.mode = Mode::Draw; + } + fn zoom_out(&mut self, p: (i32, i32)) { if self.zoom > 1 { // attempt to center around cursor @@ -309,6 +332,27 @@ impl<'ctx> AppState<'ctx> { ); } + fn draw_command_box(&mut self) { + if self.command_box.is_empty() { + self.mode = Mode::Draw; + return; + } + let (winsize_x, winsize_y) = self.canvas.window().size(); + let cmd_height: u32 = 20; + let cmd_width = winsize_x; + self.canvas.set_draw_color(WHITE); + self.canvas + .fill_rect(rect!(0, winsize_y - cmd_height, cmd_width, cmd_height)) + .unwrap(); + draw_text( + &mut self.canvas, + self.ttf_context, + &self.command_box.text[..], + BLACK, + (0, winsize_y - cmd_height), + ); + } + fn draw_mouse(&mut self) { let brush_size = self.brush_size; let cs = self.zoom as u32; @@ -368,7 +412,11 @@ impl<'ctx> AppState<'ctx> { .draw_line((line_coord, 0), (line_coord, winsize_y as i32)) .unwrap(); } - self.draw_statusline(); + if self.mode == Mode::Draw { + self.draw_statusline(); + } else { + self.draw_command_box(); + } self.draw_mouse(); } @@ -446,109 +494,175 @@ impl<'ctx> AppState<'ctx> { let mouse = event_pump.mouse_state(); self.mouse = (mouse.x(), mouse.y()); for event in event_pump.poll_iter() { - match event { - Event::KeyDown { - keycode: Some(k), .. - } => { - match k { - // pan - Keycode::W => self.pan((0, 10)), - Keycode::A => self.pan((10, 0)), - Keycode::S => self.pan((0, -10)), - Keycode::D => self.pan((-10, 0)), - // zoom - Keycode::C => { - let cursor = (mouse.x(), mouse.y()); - self.zoom_in(cursor); - } - Keycode::Z => { - let cursor = (mouse.x(), mouse.y()); - self.zoom_out(cursor); - } - // brush ops - Keycode::Q => self.decrease_brush_size(), - Keycode::E => self.increase_brush_size(), - Keycode::Num1 => self.reduce_intensity(), - Keycode::Num3 => self.increase_intensity(), - // flip color - Keycode::X => self.change_active_color(), - // toggle grid - Keycode::Tab => self.toggle_grid(), - // line drawing - Keycode::F => { - let end = (mouse.x(), mouse.y()).into(); - if let Some(start) = self.last_point { - if let Ok(o) = self.paint_line(start, end, self.active_color) { - self.commit_operation(); - self.current_operation = - o.into_iter().filter(|v| !v.old_val == v.val).collect(); - self.commit_operation(); - self.last_point = Some(end); + if let Event::KeyDown { + keycode: Some(Keycode::Num9), + keymod, + .. + } = event + { + if keymod == Mod::LSHIFTMOD || keymod == Mod::RSHIFTMOD { + self.mode = Mode::Command; + } + } + match self.mode { + Mode::Draw => { + match event { + Event::KeyDown { + keycode: Some(k), .. + } => { + match k { + // pan + Keycode::W => self.pan((0, 10)), + Keycode::A => self.pan((10, 0)), + Keycode::S => self.pan((0, -10)), + Keycode::D => self.pan((-10, 0)), + // zoom + Keycode::C => { + let cursor = (mouse.x(), mouse.y()); + self.zoom_in(cursor); } + Keycode::Z => { + let cursor = (mouse.x(), mouse.y()); + self.zoom_out(cursor); + } + // brush ops + Keycode::Q => self.decrease_brush_size(), + Keycode::E => self.increase_brush_size(), + Keycode::Num1 => self.reduce_intensity(), + Keycode::Num3 => self.increase_intensity(), + // flip color + Keycode::X => self.change_active_color(), + // toggle grid + Keycode::Tab => self.toggle_grid(), + // line drawing + Keycode::F => { + let end: Point = (mouse.x(), mouse.y()).into(); + if let Some(start) = self.last_point { + if let Ok(o) = + self.paint_line(start, end, self.active_color) + { + self.commit_operation(); + self.current_operation = o + .into_iter() + .filter(|v| !v.old_val == v.val) + .collect(); + self.commit_operation(); + self.last_point = + Some(self.idx_at_coord(end).unwrap().into()); + } + } + } + Keycode::V => self.cycle_symmetry(), + // exit + Keycode::Escape => break 'running, + // undo & redo + Keycode::U => { + if let Some(op) = self.undo_stack.undo() { + self.apply_operation(op, OpKind::Undo); + } + } + // export to file + Keycode::N => { + let image = self.export(); + let encoded = image.encode().unwrap(); + let mut buffer = File::create("test.obi").unwrap(); + eprintln!("writing to file"); + buffer.write_all(&encoded[..]).unwrap(); + } + Keycode::R => { + if let Some(op) = self.undo_stack.redo() { + self.apply_operation(op, OpKind::Redo); + } + } + _ => (), } } - Keycode::V => self.cycle_symmetry(), - // exit - Keycode::Escape => break 'running, - // undo & redo - Keycode::U => { - if let Some(op) = self.undo_stack.undo() { - self.apply_operation(op, OpKind::Undo); + // start of operation + Event::MouseButtonDown { + x, y, mouse_btn, .. + } => { + let pt = (x, y); + self.last_point = self.idx_at_coord(pt).map(MapPoint::from); + let val = match mouse_btn { + MouseButton::Right => !self.active_color, + _ => self.active_color, + }; + if let Ok(o) = self.paint_point(pt, val) { + self.current_operation.extend(o); } } - Keycode::R => { - if let Some(op) = self.undo_stack.redo() { - self.apply_operation(op, OpKind::Redo); + // click and drag + Event::MouseMotion { + x, y, mousestate, .. + } => { + if mousestate.is_mouse_button_pressed(MouseButton::Left) { + let pt = (x, y); + let val = self.active_color; + if let Ok(o) = self.paint_point(pt, val) { + self.current_operation.extend(o); + } + } else if mousestate.is_mouse_button_pressed(MouseButton::Right) { + let pt = (x, y); + let val = !self.active_color; + if let Ok(o) = self.paint_point(pt, val) { + self.current_operation.extend(o); + } } } - _ => (), + // end of operation + Event::MouseButtonUp { .. } => { + let op = self + .current_operation + .drain(..) + .filter(|v| !v.old_val == v.val) + .collect::>(); + self.undo_stack.push(op); + } + Event::Quit { .. } => { + break 'running; + } + _ => {} } } - // start of operation - Event::MouseButtonDown { - x, y, mouse_btn, .. - } => { - let pt = (x, y); - self.last_point = Some(pt.into()); - let val = match mouse_btn { - MouseButton::Right => !self.active_color, - _ => self.active_color, - }; - if let Ok(o) = self.paint_point(pt, val) { - self.current_operation.extend(o); + Mode::Command => { + if let Event::KeyDown { + keycode, keymod, .. + } = event + { + let video = self.context.video().unwrap(); + let clipboard = video.clipboard(); + if is_copy_event(keycode, keymod) { + clipboard.set_clipboard_text(&self.command_box.text); + } else if is_paste_event(keycode, keymod) + && clipboard.has_clipboard_text() + { + self.command_box.text = clipboard.clipboard_text().unwrap(); + } } - } - // click and drag - Event::MouseMotion { - x, y, mousestate, .. - } => { - if mousestate.is_mouse_button_pressed(MouseButton::Left) { - let pt = (x, y); - let val = self.active_color; - if let Ok(o) = self.paint_point(pt, val) { - self.current_operation.extend(o); + if let Event::KeyDown { + keycode: Some(k), .. + } = event + { + match k { + Keycode::Backspace => self.command_box.backspace(), + Keycode::Delete => self.command_box.delete(), + Keycode::Left => self.command_box.backward(), + Keycode::Right => self.command_box.forward(), + Keycode::Return => self.eval_command(), + Keycode::Escape => { + self.command_box.clear(); + self.mode = Mode::Draw; + } + _ => (), } - } else if mousestate.is_mouse_button_pressed(MouseButton::Right) { - let pt = (x, y); - let val = !self.active_color; - if let Ok(o) = self.paint_point(pt, val) { - self.current_operation.extend(o); + } + match event { + Event::TextInput { text, .. } => { + self.command_box.push_str(&text[..]); } + _ => (), } } - // end of operation - Event::MouseButtonUp { .. } => { - let op = self - .current_operation - .drain(..) - .filter(|v| !v.old_val == v.val) - .collect::>(); - self.undo_stack.push(op); - } - Event::Quit { .. } => { - break 'running; - } - _ => {} } } self.redraw(); diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 0000000..3beb700 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,134 @@ +#[derive(Debug)] +pub struct CommandBox { + pub enabled: bool, + pub history: Vec, + pub text: String, + pub cursor: usize, +} + +// cursor value of 0 is behind all text +// cursor value of n is after n characters (insert after index n - 1) +// cursor value of text.len() is after all text + +impl CommandBox { + pub fn new() -> Self { + CommandBox { + enabled: false, + history: vec![], + text: String::new(), + cursor: 0, + } + } + + pub fn forward(&mut self) { + if self.cursor < self.text.len() { + self.cursor += 1; + } + } + + pub fn backward(&mut self) { + self.cursor = self.cursor.saturating_sub(1); + } + + pub fn backspace(&mut self) { + if self.cursor != 0 { + self.text.remove(self.cursor - 1); + self.backward(); + } + } + + pub fn delete(&mut self) { + if self.cursor < self.text.len() { + self.text.remove(self.cursor); + } + } + + pub fn push_str(&mut self, v: &str) { + self.text.push_str(v); + self.cursor += v.len(); + } + + pub fn is_empty(&self) -> bool { + self.text.is_empty() + } + + pub fn clear(&mut self) { + self.text.clear(); + self.cursor = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn setup_with(text: &str) -> CommandBox { + let mut cmd = CommandBox::new(); + cmd.push_str(text); + cmd + } + + #[test] + fn entering_text() { + let cmd = setup_with("save as file.png"); + assert_eq!(&cmd.text, "save as file.png"); + assert_eq!(cmd.cursor, 16) + } + + #[test] + fn backspacing_from_end() { + let mut cmd = setup_with("save"); + cmd.backspace(); + assert_eq!(&cmd.text, "sav"); + assert_eq!(cmd.cursor, 3); + } + + #[test] + fn backspacing_from_middle() { + let mut cmd = setup_with("save"); + cmd.backward(); + cmd.backspace(); + assert_eq!(&cmd.text, "sae"); + assert_eq!(cmd.cursor, 2); + } + + #[test] + fn delete() { + let mut cmd = setup_with("save"); + cmd.backward(); + cmd.delete(); + assert_eq!(&cmd.text, "sav"); + assert_eq!(cmd.cursor, 3); + } + + #[test] + fn delete_end() { + let mut cmd = setup_with("save"); + cmd.delete(); + assert_eq!(&cmd.text, "save"); + } + + #[test] + fn delete_all() { + let mut cmd = setup_with("save"); + for _ in 0..4 { + cmd.backward(); + } + for _ in 0..4 { + cmd.delete(); + } + assert_eq!(&cmd.text, ""); + assert_eq!(cmd.cursor, 0); + } + + #[test] + fn seeking() { + let mut cmd = setup_with("save"); + for _ in 0..4 { + cmd.backward(); + } + assert_eq!(cmd.cursor, 0); + cmd.forward(); + assert_eq!(cmd.cursor, 1); + } +} diff --git a/src/main.rs b/src/main.rs index 14f4cf5..ebdf793 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,6 @@ mod app; mod bitmap; +mod command; mod consts; mod dither; mod symmetry; @@ -8,8 +9,41 @@ mod utils; use app::AppState; +use std::{ + env, + fs::OpenOptions, + io::{Cursor, Read}, +}; + +use obi::Image; + pub fn main() { let sdl_context = sdl2::init().unwrap(); let ttf_context = sdl2::ttf::init().unwrap(); - AppState::init(200, 200, &sdl_context, &ttf_context).run(); + let args: Vec<_> = env::args().collect(); + if args.len() < 2 { + AppState::init(200, 200, &sdl_context, &ttf_context, None).run(); + return; + } else { + let path = args.get(1).unwrap(); + let image_src = OpenOptions::new() + .read(true) + .write(false) + .create(false) + .open(path); + if let Ok(mut image) = image_src { + let mut buf = Vec::new(); + image.read_to_end(&mut buf).unwrap(); + let decoded = Image::decode(&mut (Cursor::new(buf))).unwrap(); + let (width, height) = (decoded.width(), decoded.height()); + AppState::init( + width, + height, + &sdl_context, + &ttf_context, + Some(decoded.data), + ) + .run(); + } + } } diff --git a/src/utils.rs b/src/utils.rs index a1b3624..71a9eea 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,11 @@ use crate::consts::FONT_PATH; -use sdl2::{pixels::Color, render::Canvas, ttf::Sdl2TtfContext, video::Window}; +use sdl2::{ + keyboard::{Keycode, Mod}, + pixels::Color, + render::Canvas, + ttf::Sdl2TtfContext, + video::Window, +}; #[macro_export] macro_rules! rect( @@ -27,4 +33,13 @@ pub fn draw_text>( let (width, height) = font.size_of_latin1(text.as_bytes()).unwrap(); let area = rect!(x, y, width, height); canvas.copy(&texture, None, area).unwrap(); + width +} + +pub fn is_copy_event(keycode: Option, keymod: Mod) -> bool { + keycode == Some(Keycode::C) && (keymod == Mod::LCTRLMOD || keymod == Mod::RCTRLMOD) +} + +pub fn is_paste_event(keycode: Option, keymod: Mod) -> bool { + keycode == Some(Keycode::V) && (keymod == Mod::LCTRLMOD || keymod == Mod::RCTRLMOD) } -- cgit v1.2.3