From 2c4455f623259eda41dc04724c9d73fd70cdde59 Mon Sep 17 00:00:00 2001 From: stjet <49297268+stjet@users.noreply.github.com> Date: Tue, 12 Aug 2025 06:53:57 +0000 Subject: [PATCH] lines, barebones drawing window addition of lines means ipc slightly changed, though can be ignored. also, minor malvim fix --- Cargo.toml | 6 +- docs/window-likes/draw.md | 14 ++ install | 1 + local-install | 1 + ming-wm-lib/src/serialize.rs | 30 +++ ming-wm-lib/src/window_manager_types.rs | 9 +- src/bin/draw.rs | 235 ++++++++++++++++++++++++ src/bin/malvim.rs | 15 +- wm/src/framebuffer.rs | 29 +++ wm/src/window_manager.rs | 19 +- 10 files changed, 346 insertions(+), 13 deletions(-) create mode 100644 docs/window-likes/draw.md create mode 100644 src/bin/draw.rs diff --git a/Cargo.toml b/Cargo.toml index 51518bb..0d2e9ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ming-wm" -version = "1.1.0" +version = "1.2.0-beta.0" repository = "https://github.com/stjet/ming-wm" license = "GPL-3.0-or-later" edition = "2021" @@ -63,3 +63,7 @@ path = "src/bin/malvim.rs" [[bin]] name = "mingGames_Reversi" path = "src/bin/reversi.rs" + +[[bin]] +name = "mingUtils_Draw" +path = "src/bin/draw.rs" \ No newline at end of file diff --git a/docs/window-likes/draw.md b/docs/window-likes/draw.md new file mode 100644 index 0000000..383a053 --- /dev/null +++ b/docs/window-likes/draw.md @@ -0,0 +1,14 @@ +Very basic drawing board. + +## Moving + +Arrow keys or hjkl. `i` to enter input mode. Use the enter key to finish a line or rectangle. Escape to cancel line or rectangle in progress. + +## Input Commands + +- `line`: Start line with current point as start. Move and hit enter to determine line endpoint +- `rect`: Start rect with current point as a corner. Move and hit enter to determine the opposite corner +- `color/c/colour [lowercase 6 char hex string]`: Set current colour +- `linewidth/lw [int]`: Set current line width +- `undo`: Undo last draw +- `clear`: Clear all drawings \ No newline at end of file diff --git a/install b/install index b2f98eb..992d624 100755 --- a/install +++ b/install @@ -16,3 +16,4 @@ cp ./target/release/mingGames_Minesweeper /usr/local/bin/mingGames_Minesweeper cp ./target/release/mingFiles_File_Explorer /usr/local/bin/mingFiles_File_Explorer cp ./target/release/mingFiles_Audio_Player /usr/local/bin/mingFiles_Audio_Player cp ./target/release/mingEditing_Malvim /usr/local/bin/mingEditing_Malvim +cp ./target/release/mingUtils_Draw /usr/local/bin/mingUtils_Draw diff --git a/local-install b/local-install index 77de66a..76bc44a 100755 --- a/local-install +++ b/local-install @@ -16,3 +16,4 @@ cp ./target/release/mingGames_Minesweeper ~/.local/bin/mingGames_Minesweeper cp ./target/release/mingFiles_File_Explorer ~/.local/bin/mingFiles_File_Explorer cp ./target/release/mingFiles_Audio_Player ~/.local/bin/mingFiles_Audio_Player cp ./target/release/mingEditing_Malvim ~/.local/bin/mingEditing_Malvim +cp ./target/release/mingUtils_Draw ~/.local/bin/mingUtils_Draw diff --git a/ming-wm-lib/src/serialize.rs b/ming-wm-lib/src/serialize.rs index 12e3ed1..e172ad2 100644 --- a/ming-wm-lib/src/serialize.rs +++ b/ming-wm-lib/src/serialize.rs @@ -221,6 +221,7 @@ impl Serializable for DrawInstructions { DrawInstructions::Gradient(p, d, c1, c2, u) => format!("Gradient/{}\x1E{}\x1E{}\x1E{}\x1E{}", array_to_string(p), array_to_string(d), array_to_string(c1), array_to_string(c2), u), DrawInstructions::Bmp(p, s, b) => format!("Bmp/{}\x1E{}\x1E{}", array_to_string(p), s, b), DrawInstructions::Circle(p, u, c) => format!("Circle/{}\x1E{}\x1E{}", array_to_string(p), u, array_to_string(c)), + DrawInstructions::Line(s, e, w, c) => format!("Line/{}\x1E{}\x1E{}\x1E{}", array_to_string(s), array_to_string(e), w, array_to_string(c)), } } fn deserialize(serialized: &str) -> Result { @@ -402,6 +403,35 @@ impl Serializable for DrawInstructions { let c = get_color(arg.unwrap())?; Ok(DrawInstructions::Circle(p, u.unwrap(), c)) }, + "Line" => { + let rest = get_rest_of_split(&mut parts, Some("/")); + //(s, e, w, c) + let mut args = rest.split("\x1E"); + let arg = args.next(); + if arg.is_none() { + return Err(()); + } + let s = get_two_array(arg.unwrap())?; + let arg = args.next(); + if arg.is_none() { + return Err(()); + } + let e = get_two_array(arg.unwrap())?; + let arg = args.next(); + if arg.is_none() { + return Err(()); + } + let w = arg.unwrap().parse(); + if w.is_err() { + return Err(()); + } + let arg = args.next(); + if arg.is_none() { + return Err(()); + } + let c = get_color(arg.unwrap())?; + Ok(DrawInstructions::Line(s, e, w.unwrap(), c)) + }, _ => Err(()), } } diff --git a/ming-wm-lib/src/window_manager_types.rs b/ming-wm-lib/src/window_manager_types.rs index fd1ba26..2f27422 100644 --- a/ming-wm-lib/src/window_manager_types.rs +++ b/ming-wm-lib/src/window_manager_types.rs @@ -14,11 +14,18 @@ pub enum KeyChar { #[derive(Debug)] pub enum DrawInstructions { + /// Top left point, dimensions, colour Rect(Point, Dimensions, RGBColor), - Text(Point, Vec, String, RGBColor, RGBColor, Option, Option), //font and text + /// Top left point, fonts, text, colour, background colour, horizontal spacing, monospace width + Text(Point, Vec, String, RGBColor, RGBColor, Option, Option), + /// Top left point, dimensions, start colour, end colour, steps Gradient(Point, Dimensions, RGBColor, RGBColor, usize), + /// Top left point, path to file, reverse Bmp(Point, String, bool), + /// Centre point, radius, colour Circle(Point, usize, RGBColor), + /// Start point, end point, line width, line colour + Line(Point, Point, usize, RGBColor), } #[derive(Debug, PartialEq)] diff --git a/src/bin/draw.rs b/src/bin/draw.rs new file mode 100644 index 0000000..ea22446 --- /dev/null +++ b/src/bin/draw.rs @@ -0,0 +1,235 @@ +use std::vec::Vec; +use std::vec; + +use ming_wm_lib::window_manager_types::{ DrawInstructions, WindowLike, WindowLikeType }; +use ming_wm_lib::messages::{ WindowMessage, WindowMessageResponse }; +use ming_wm_lib::framebuffer_types::{ Dimensions, Point, RGBColor }; +use ming_wm_lib::themes::ThemeInfo; +use ming_wm_lib::utils::{ hex_to_u8, HEX_CHARS, Substring }; +use ming_wm_lib::ipc::listen; + +enum DrawAction { + Line(Point, Option, usize, RGBColor), + Rect(Point, Option, RGBColor), + // +} + +impl DrawAction { + fn name(&self) -> String { + match self { + DrawAction::Line(_, _, _, _) => "Line", + DrawAction::Rect(_, _, _) => "Rect", + }.to_string() + } +} + +#[derive(Default, PartialEq)] +enum Mode { + #[default] + Move, + Input, +} + +#[derive(Default)] +struct Draw { + mode: Mode, + dimensions: Dimensions, + draw_actions: Vec, + current_location: Point, + current_input: String, + current_color: RGBColor, + current_linewidth: usize, + current_action: Option, +} + +impl WindowLike for Draw { + fn handle_message(&mut self, message: WindowMessage) -> WindowMessageResponse { + match message { + WindowMessage::Init(dimensions) => { + self.dimensions = dimensions; + WindowMessageResponse::JustRedraw + }, + WindowMessage::KeyPress(key_press) => { + if key_press.is_escape() && (self.current_action.is_some() || self.mode != Mode::Move) { + self.current_action = None; + self.mode = Mode::Move; + self.current_input = String::new(); + WindowMessageResponse::JustRedraw + } else if self.mode == Mode::Input { + if key_press.is_backspace() && self.current_input.len() > 0 { + self.current_input = self.current_input.remove_last(); + WindowMessageResponse::JustRedraw + } else if key_press.is_enter() { + //process current input + let mut parts = self.current_input.split(" "); + match parts.next().unwrap() { + "line" | "l" => { + self.current_action = Some(DrawAction::Line(self.current_location, None, self.current_linewidth, self.current_color)); + }, + "rect" | "r" => { + self.current_action = Some(DrawAction::Rect(self.current_location, None, self.current_color)); + }, + "colour" | "color" | "c" => { + //hex to u8 + if let Some(hex_color) = parts.next() { + if hex_color.len() == 6 && hex_color.chars().all(|c| HEX_CHARS.contains(&c)) { + let mut hex_chars = hex_color.chars(); + self.current_color = [hex_to_u8(hex_chars.next().unwrap(), hex_chars.next().unwrap()), hex_to_u8(hex_chars.next().unwrap(), hex_chars.next().unwrap()), hex_to_u8(hex_chars.next().unwrap(), hex_chars.next().unwrap())]; + } + } + }, + "linewidth" | "lw" => { + if let Ok(linewidth) = parts.next().unwrap_or("").parse::() { + self.current_linewidth = linewidth; + } + }, + "undo" | "u" => { + self.draw_actions.pop(); + }, + "clear" | "cl" => { + self.draw_actions = Vec::new(); + }, + _ => {}, + }; + self.mode = Mode::Move; + self.current_input = String::new(); + WindowMessageResponse::JustRedraw + } else if key_press.is_regular() { + self.current_input += &key_press.key.to_string(); + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else if key_press.key == 'i' && self.current_action.is_none() { + self.mode = Mode::Input; + WindowMessageResponse::JustRedraw + } else if key_press.is_enter() { + if let Some(current_action) = &self.current_action { + self.draw_actions.push(match current_action { + DrawAction::Line(p, _, u, r) => DrawAction::Line(*p, Some(self.current_location), *u, *r), + DrawAction::Rect(p, _, r) => { + let d = [ + if self.current_location[0] > p[0] { + self.current_location[0] - p[0] + } else { + p[0] - self.current_location[0] + }, + if self.current_location[1] > p[1] { + self.current_location[1] - p[1] + } else { + p[1] - self.current_location[1] + } + ]; + //find top left corner + let tl = [ + if p[0] < self.current_location[0] { + p[0] + } else { + self.current_location[0] + }, + if p[1] < self.current_location[1] { + p[1] + } else { + self.current_location[1] + } + ]; + DrawAction::Rect(tl, Some(d), *r) + }, + }); + self.current_action = None; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else if key_press.is_up_arrow() || key_press.key == 'k' { + if self.current_location[1] > 0 { + self.current_location[1] -= 1; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else if key_press.is_down_arrow() || key_press.key == 'j' { + if self.current_location[1] + 1 < self.dimensions[1] { + self.current_location[1] += 1; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else if key_press.is_left_arrow() || key_press.key == 'h' { + if self.current_location[0] > 0 { + self.current_location[0] -= 1; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else if key_press.is_right_arrow() || key_press.key == 'l' { + if self.current_location[0] + 1 < self.dimensions[0] { + self.current_location[0] += 1; + WindowMessageResponse::JustRedraw + } else { + WindowMessageResponse::DoNothing + } + } else { + WindowMessageResponse::DoNothing + } + }, + _ => WindowMessageResponse::DoNothing, + } + } + + fn draw(&self, theme_info: &ThemeInfo) -> Vec { + let mut instructions = Vec::new(); + //draw previous actions + for action in &self.draw_actions { + instructions.push(match action { + DrawAction::Line(p1, p2, lw, c) => DrawInstructions::Line(*p1, p2.unwrap(), *lw, *c), + DrawAction::Rect(p, d, c) => DrawInstructions::Rect(*p, d.unwrap(), *c), + }); + } + //draw cursor (crosshair) + let crosshair_min_x = self.current_location[0].checked_sub(6).unwrap_or(0); + let crosshair_min_y = self.current_location[1].checked_sub(6).unwrap_or(0); + //^going over should be handled by the drawer, probably? + instructions.push(DrawInstructions::Line([crosshair_min_x, self.current_location[1]], [self.current_location[0] + 6, self.current_location[1]], 1, self.current_color)); + instructions.push(DrawInstructions::Line([self.current_location[0], crosshair_min_y], [self.current_location[0], self.current_location[1] + 6], 1, self.current_color)); + //draw info or current input + instructions.push(DrawInstructions::Text([2, self.dimensions[1] - 19], vec!["nimbus-roman".to_string()], if self.current_input == String::new() { + if let Some(current_action) = &self.current_action { + current_action.name() + } else if self.mode == Mode::Move { + "'i' to enter input mode".to_string() + } else { + "Awaiting input".to_string() + } + } else { + self.current_input.clone() + }, theme_info.text, theme_info.background, None, None)); + instructions + } + + //properties + + fn title(&self) -> String { + "Draw".to_string() + } + + fn subtype(&self) -> WindowLikeType { + WindowLikeType::Window + } + + fn ideal_dimensions(&self, _dimensions: Dimensions) -> Dimensions { + [410, 410] + } +} + +impl Draw { + pub fn new() -> Self { + let mut d: Self = Default::default(); + d.current_linewidth = 1; + d + } +} + +pub fn main() { + listen(Draw::new()); +} diff --git a/src/bin/malvim.rs b/src/bin/malvim.rs index 3c4879d..c9ea350 100644 --- a/src/bin/malvim.rs +++ b/src/bin/malvim.rs @@ -168,11 +168,14 @@ impl WindowLike for Malvim { self.state = State::None; } else if self.state == State::MaybeDelete { if key_press.key == 'd' { - current_file.content.remove(current_file.line_pos); - if current_file.content.len() == 0 { - current_file.content = vec![String::new()]; - } else if current_file.line_pos == current_file.content.len() { - current_file.line_pos = current_file.content.len() - 1; + for _ in 0..self.maybe_num.unwrap_or(1) { + current_file.content.remove(current_file.line_pos); + if current_file.content.len() == 0 { + current_file.content = vec![String::new()]; + } else if current_file.line_pos == current_file.content.len() { + current_file.line_pos = current_file.content.len() - 1; + break; + } } let new_length = current_file.content[current_file.line_pos].chars().count(); current_file.cursor_pos = calc_new_cursor_pos(current_file.cursor_pos, new_length); @@ -392,7 +395,7 @@ impl WindowLike for Malvim { changed = false; } //reset maybe_num if not num - if !numbered && self.state != State::Maybeg { + if !numbered && self.state != State::Maybeg && self.state != State::MaybeDelete { self.maybe_num = None; } } else if self.mode == Mode::Command { diff --git a/wm/src/framebuffer.rs b/wm/src/framebuffer.rs index 85de2cd..fe68a3f 100644 --- a/wm/src/framebuffer.rs +++ b/wm/src/framebuffer.rs @@ -108,6 +108,7 @@ impl FramebufferWriter { .copy_from_slice(&color[..]); } + //straight horizontal line fn _draw_line(&mut self, start_pos: usize, bytes: &[u8]) { self.buffer[start_pos..(start_pos + bytes.len())] .copy_from_slice(bytes); @@ -244,6 +245,34 @@ impl FramebufferWriter { } } + //line + + pub fn draw_line(&mut self, start: Point, end: Point, width: usize, color: RGBColor) { + //leftmost point + let lm; + let rm; + if start[0] < end[0] || (start[0] == end[0] && end[1] > start[1]) { + lm = start; + rm = end; + } else { + lm = end; + rm = start; + }; + let dx = (rm[0] - lm[0]) as f64; //will not be negative + let dy = rm[1] as f64 - lm[1] as f64; //surely no point will be big enough to lose data here? + let use_x = dy.abs() < dx; + //slope + let m = if use_x { dy / dx } else { dx / dy }; + for ci in 0..if use_x { dx as usize } else { dy.abs() as usize } { + let i = if !use_x && dy < 0.0 { -(ci as f64) } else { ci as f64 }; + let ix = if use_x { i } else { (m * i).round() }; + let iy = if use_x { (m * i).round() } else { i }; + for j in 0..width { + self.draw_pixel([(lm[0] as f64 + ix) as usize + j, (lm[1] as f64 + iy) as usize], color); + } + } + } + //bmps //reverse is workaround for when my bmp lib returns rgba instead of bgra diff --git a/wm/src/window_manager.rs b/wm/src/window_manager.rs index 948e259..bfd5229 100644 --- a/wm/src/window_manager.rs +++ b/wm/src/window_manager.rs @@ -648,6 +648,13 @@ impl WindowManager { [top_left[0], top_left[1] + if is_window { WINDOW_TOP_HEIGHT } else { 0 }] } + fn prevent_overflow_dimensions(dimensions: Dimensions, window_dimensions: Dimensions, top_left: Point) -> Dimensions { + [ + min(dimensions[0], window_dimensions[0] - top_left[0]), + min(dimensions[1], window_dimensions[1] - top_left[1]), + ] + } + //another issue with a huge vector of draw instructions; it takes up heap memory pub fn draw(&mut self, maybe_redraw_ids: Option>, use_saved_buffer: bool) { let theme_info = get_theme_info(&self.theme).unwrap(); @@ -694,6 +701,7 @@ impl WindowManager { DrawInstructions::Text(top_left, fonts, text, color, bg_color, horiz_spacing, mono_width) => DrawInstructions::Text(WindowManager::get_true_top_left(top_left, is_window), fonts.clone(), text.clone(), *color, *bg_color, *horiz_spacing, *mono_width), DrawInstructions::Bmp(top_left, path, reverse) => DrawInstructions::Bmp(WindowManager::get_true_top_left(top_left, is_window), path.to_string(), *reverse), DrawInstructions::Gradient(top_left, dimensions, start_color, end_color, steps) => DrawInstructions::Gradient(WindowManager::get_true_top_left(top_left, is_window), *dimensions, *start_color, *end_color, *steps), + DrawInstructions::Line(start, end, width, color) => DrawInstructions::Line(WindowManager::get_true_top_left(start, is_window), WindowManager::get_true_top_left(end, is_window), *width, *color), } }).collect(); //draw window background @@ -729,10 +737,7 @@ impl WindowManager { match instruction { DrawInstructions::Rect(top_left, dimensions, color) => { //try and prevent overflows out of the window - let true_dimensions = [ - min(dimensions[0], window_dimensions[0] - top_left[0]), - min(dimensions[1], window_dimensions[1] - top_left[1]), - ]; + let true_dimensions = WindowManager::prevent_overflow_dimensions(dimensions, window_dimensions, top_left); window_writer.draw_rect(top_left, true_dimensions, color); }, DrawInstructions::Circle(centre, radius, color) => { @@ -746,7 +751,11 @@ impl WindowManager { window_writer.draw_bmp(top_left, path, reverse); }, DrawInstructions::Gradient(top_left, dimensions, start_color, end_color, steps) => { - window_writer.draw_gradient(top_left, dimensions, start_color, end_color, steps); + let true_dimensions = WindowManager::prevent_overflow_dimensions(dimensions, window_dimensions, top_left); + window_writer.draw_gradient(top_left, true_dimensions, start_color, end_color, steps); + }, + DrawInstructions::Line(start, end, width, color) => { + window_writer.draw_line(start, end, width, color); }, } }