From bfbff5a7ea4e40a82ad9c2fb592f46266d4d836b Mon Sep 17 00:00:00 2001 From: Dukk Date: Mon, 4 Sep 2023 12:27:26 -0400 Subject: [PATCH] refactor: split up some of the files --- src/README.md | 15 ++ src/app.rs | 391 +++----------------------------------------------- src/gui.rs | 364 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 4 +- 4 files changed, 398 insertions(+), 376 deletions(-) create mode 100644 src/README.md create mode 100644 src/gui.rs diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..f0f7e4a --- /dev/null +++ b/src/README.md @@ -0,0 +1,15 @@ +It's in here I'll try to breakdown the architecture for anyone who may wish to contribute in the future. It's quite simple, really. + +# Architecture +The code is split between `app.rs` and `gui.rs`. `app.rs` contains the business logic of the app; it has the state, multiple utility types, and the implmentation of the app itself. `gui.rs`, on the other hand, contains the GUI logic; it implements `eframe::App` and handles updating the screen, rendering the GUI, etc. + +Since `update` already has a mutable borrow of `self`, the implementation of `PathyApp` has to manually take in mutable references to whatever it will modify, since the borrow checker won't allow us a second mutable borrow of `self`. + +## Rendering +The bulk of the code is really in the GUI. Every frame it checks the current cursor mode, and renders a tooltip based off of that. It then handles any mouse input, drags, etc., and also renders the overlay for the user. Since `egui` is an immediate mode GUI library(GUI is rerendered every frame), it makes updating the application state quite easy; as any change in state will immediately be rendered later in this frame or in the next one. + +## The Magic +Of course, the "magic" is in the pre-processing and generation steps. The pre-processer rounds off all the measurements. Since we're calculating all the angles and distances here, we just save those (which makes our generation function trivial). Here's some notable things about the preprocessing calculations: + +### The Distance +The distance is just calculated using the cosine of the angle. However, since the angle can sometimes be 90 degrees, this can occasionally bug out. Therefore, we have an additional check to prevent issues with divide by zero, as the angle calculation itself uses the slope, and 90 degree angles result in a slope of `Δy/0`. diff --git a/src/app.rs b/src/app.rs index 806954d..9d1c1ca 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,60 +1,25 @@ -/* - * PATHY - A tool for creating complex paths - * in autonomous code without having to manually write - * each and every variable. - * - * Created by Daksh Gupta. - */ -use eframe::egui::Visuals; -use egui::{pos2, Color32, Pos2, Rect, Sense, Stroke, Vec2}; +use egui::Pos2; use egui_extras::RetainedImage; -// Uncomment this section to get access to the console_log macro -// Use console_log to print things to console. println macro doesn't work -// here, so you'll need it. -/*use wasm_bindgen::prelude::*; -#[wasm_bindgen] -extern "C" { - // Use `js_namespace` here to bind `console.log(..)` instead of just - // `log(..)` - #[wasm_bindgen(js_namespace = console)] - fn log(s: &str); - // The `console.log` is quite polymorphic, so we can bind it with multiple - // signatures. Note that we need to use `js_name` to ensure we always call - // `log` in JS. - #[wasm_bindgen(js_namespace = console, js_name = log)] - fn log_u32(a: u32); - - // Multiple arguments too! - #[wasm_bindgen(js_namespace = console, js_name = log)] - fn log_many(a: &str, b: &str); -} -macro_rules! console_log { - // Note that this is using the `log` function imported above during - // `bare_bones` - ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) -} -// */ -/// We derive Deserialize/Serialize so we can persist app state on shutdown. #[derive(serde::Deserialize, serde::Serialize)] #[serde(default)] // if we add new fields, give them default values when deserializing old state pub struct PathyApp { // this how you opt-out of serialization of a member //#[serde(skip)] - height: f32, // Height of field - width: f32, // Width of field - scale: f32, // Scale to display - mode: CursorMode, // Cursor mode - path: Vec, // Current path - selected: usize, // Current selected node (edit mode) - processed: Vec, // Processed fields + pub height: f32, // Height of field + pub width: f32, // Width of field + pub scale: f32, // Scale to display + pub mode: CursorMode, // Cursor mode + pub path: Vec, // Current path + pub selected: usize, // Current selected node (edit mode) + pub processed: Vec, // Processed fields #[serde(skip)] // We can't serialize and image; and we don't want to - overlay: Option, // Uploaded overlay - result: Option, // Final string + pub overlay: Option, // Uploaded overlay + pub result: Option, // Final string } #[derive(serde::Deserialize, serde::Serialize, Eq, PartialEq)] -enum CursorMode { +pub enum CursorMode { // Represent possible cursor modes Default, // No action Create, // Create new nodes and paths @@ -64,7 +29,7 @@ enum CursorMode { } #[derive(serde::Deserialize, serde::Serialize, Clone)] -enum Process { +pub enum Process { // Represents movements for the robot to take Drive(i32), Turn(i32), @@ -179,7 +144,7 @@ impl PathyApp { Default::default() } /// Preprocess the route to round all integers - fn preprocess(path: &mut Vec, from: (f32, f32), to: (f32, f32)) -> Vec { + pub fn preprocess(path: &mut Vec, from: (f32, f32), to: (f32, f32)) -> Vec { /* To create the optimal route, we don't want to rely on somewhat imprecise and arbitrary * integers. Hence, all distances are rounded to the nearest inch. We also use the slope to * calculate the angle that the robot will need to be turned(again, rounded). Then, we'll @@ -200,8 +165,8 @@ impl PathyApp { let start: Pos2 = path[0]; // Store the previous point let mut prev = path.remove(0); - // Store the previous angle - let mut prev_angle = ComplexAngle::new(90, true); // We start facing "up" + // Store the previous angle - we start facing "up" might make it an option later + let mut prev_angle = ComplexAngle::new(90, true); let grouped_processes: Vec<(i32, i32, i32)> = path .iter() .map(|pos| { @@ -267,7 +232,7 @@ impl PathyApp { .as_slice() .concat() } - fn generate(processes: &Vec) -> String { + pub fn generate(processes: &Vec) -> String { let mut result = String::from("// The following code was generated by Pathy:"); processes.iter().for_each(|process| match *process { Process::Turn(angle) => { @@ -293,327 +258,3 @@ impl PathyApp { result } } - -impl eframe::App for PathyApp { - /// Called by the frame work to save state before shutdown. - fn save(&mut self, storage: &mut dyn eframe::Storage) { - eframe::set_value(storage, eframe::APP_KEY, self); - } - - /// Called each time the UI needs repainting, which may be many times per second. - /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. - fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { - // Dark mode - ctx.set_visuals(Visuals::dark()); - let Self { - height, - width, - scale, - mode, - path, - selected, - overlay, - result, - processed, - } = self; - - #[cfg(not(target_arch = "wasm32"))] // no File->Quit on web pages! - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - // The top panel is often a good place for a menu bar: - egui::menu::bar(ui, |ui| { - ui.menu_button("File", |ui| { - if ui.button("Quit").clicked() { - _frame.close(); - } - }); - }); - }); - - egui::SidePanel::left("side_panel").show(ctx, |ui| { - ui.heading("Settings"); - - // Size settings - let response = ui.add_enabled_ui(path.len() == 0, |ui| { - ui.label("Field Dimensions:"); - ui.horizontal(|ui| { - ui.add(egui::DragValue::new(height)); - ui.label("Height (Inches)") - }); - ui.horizontal(|ui| { - ui.add(egui::DragValue::new(width)); - ui.label("Width (Inches)") - }); - ui.add(egui::Slider::new(scale, 0.0..=1000.0).text("Scale")); - }); - if path.len() != 0 { - response.response.on_hover_text_at_pointer( - "Size settings may not be changed once you've created a path.", - ); - } - ui.label("Drop an image to set an overlay!"); - - // Notice - ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { - ui.horizontal(|ui| { - ui.spacing_mut().item_spacing.x = 0.0; - ui.label("Made for "); - ui.hyperlink_to("EZTemplate", "https://ez-robotics.github.io/EZ-Template/"); - ui.label(" and "); - ui.hyperlink_to("PROS", "https://pros.cs.purdue.edu"); - ui.label("."); - }); - }); - }); - - egui::CentralPanel::default().show(ctx, |ui| { - // The central panel the region left after adding TopPanel's and SidePanel's - - ui.heading("Pathy"); - ui.label("Created by Daksh Gupta."); - ui.label("Made for use in auton code. Generated code uses the EZTemplate API."); - egui::warn_if_debug_build(ui); - }); - - // Path Designer - egui::Window::new("Path Designer").show(ctx, |ui| { - // If the width is scale, find the height that keeps it - // in the correct aspect ratio - let aspecty: f32 = (*height / *width) * *scale; - ui.heading("Path Designer"); - // UI Buttons - ui.horizontal(|ui| { - if ui.button("Create").clicked() { - *mode = CursorMode::Create; - // Reset processes - *processed = Vec::new(); - } - if ui.button("Edit").clicked() { - *mode = CursorMode::Edit; - *processed = Vec::new(); - } - if ui.button("Delete").clicked() { - *mode = CursorMode::Delete; - *processed = Vec::new(); - } - if ui.button("Trim").clicked() { - *mode = CursorMode::Trim; - *processed = Vec::new(); - } - if ui.button("Clear").clicked() { - *path = Vec::new(); // Clear path - *processed = Vec::new(); - *result = None; - } - if ui.button("Preprocess").clicked() { - *processed = Self::preprocess(path, (*scale, aspecty), (*width, *height)); - } - // Order here is important, as the ui button is only rendered if the first - // condition is true. Otherwise, there's no point in evaluating the second - // condition, thus not rendering the button. - if processed.len() != 0 && ui.button("Generate").clicked() { - *result = Some(Self::generate(processed)); - } - if *mode != CursorMode::Default && ui.button("Finish").clicked() { - *mode = CursorMode::Default; - } - }); - ui.add_space(10.0); - // Render the bounds - let (rect, response) = ui.allocate_at_least( - Vec2 { - x: *scale, - y: aspecty, - }, - Sense::click_and_drag(), - ); - ui.painter().rect( - rect, - 0.0, - Color32::from_gray(64), - Stroke::new(5.0, Color32::WHITE), - ); - // Check for dropped files - ctx.input(|i| match i.raw.dropped_files.last() { - Some(file) => match file.clone().bytes { - Some(bytes) => match RetainedImage::from_image_bytes("", &*bytes) { - Ok(image) => *overlay = Some(image), - Err(_) => (), - }, - None => (), - }, - None => (), - }); - // Render image - match overlay { - Some(image) => { - //image.show_scaled(ui, *scale / (image.width() as f32)); - ui.painter().image( - image.texture_id(ctx), - rect, - Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), - Color32::WHITE, - ); - } - None => (), - } - // Variable we'll use to render lines - let mut prev: Option = None; - // Line stroke - let yellow_line = Stroke { - width: 3.0, - color: Color32::YELLOW, - }; - // Render all lines - path.iter().for_each(|pos| { - let screen_pos = Pos2 { - x: response.rect.min.x + pos.x, - y: response.rect.min.y + pos.y, - }; - // Render lines - match prev { - Some(prev_pos) => { - ui.painter() - .line_segment([prev_pos, screen_pos], yellow_line); - } - None => (), - }; - prev = Some(screen_pos); - }); - // Render all points - path.iter().enumerate().for_each(|(idx, pos)| { - let screen_pos = Pos2 { - x: response.rect.min.x + pos.x, - y: response.rect.min.y + pos.y, - }; - // Render points - if idx == 0 { - ui.painter().circle_filled(screen_pos, 5.0, Color32::GREEN); - } else if idx == path.len() - 1 { - ui.painter().circle_filled(screen_pos, 5.0, Color32::BLUE); - } else { - ui.painter().circle_filled(screen_pos, 5.0, Color32::YELLOW); - } - }); - // Hovered point, Edit and Delete will actually set this to be the closest point. - let mut hovered = response.hover_pos(); - // Index of selected point of path (used by edit & delete) - let mut sl_idx: Option = None; - // Render the tooltips - match *mode { - CursorMode::Default => (), // No tooltip - CursorMode::Create => { - // Get pointer position - match hovered { - // Put circle under cursor - Some(pos) => { - ui.painter().circle_filled(pos, 5.0, Color32::YELLOW); - // Render line preview - match prev { - Some(prev_pos) => { - ui.painter().line_segment([prev_pos, pos], yellow_line) - } - None => (), - } - } - None => (), - } - } - CursorMode::Edit | CursorMode::Delete | CursorMode::Trim => { - // Get pointer position - match hovered { - Some(hover_pos) => { - // Find the nearest point. We just add the x and y differences without - // squaring them, since we don't need the actual distance, just - // something we can compare (and works 99% of the time). - let mut distance = f32::MAX; - hovered = Some(path.iter().enumerate().fold( - hover_pos, - |old_pos, (idx, pos)| { - let screen_pos = Pos2 { - x: response.rect.min.x + pos.x, - y: response.rect.min.y + pos.y, - }; - let dis = f32::abs(hover_pos.x - screen_pos.x) - + f32::abs(hover_pos.y - screen_pos.y); - if dis < distance { - distance = dis; - sl_idx = Some(idx); - return screen_pos; - } - old_pos - }, - )); - // Render closest point red - ui.painter() - .circle_filled(hovered.unwrap(), 5.1, Color32::RED); - } - None => (), - } - } - } - // Handle clicks - // I'd like to do this first, so there's no frame delay, but it's a little more - // idiomatic for me to do it this way, since we can now use a match statement above - // (since Delete mode uses the nearest point calculated for the tooltip). - if response.clicked() { - match mode { - // Default does nothing, and Edit uses drags - CursorMode::Default | CursorMode::Edit => (), - // Add cursor position to list - CursorMode::Create => match ctx.pointer_interact_pos() { - Some(pos) => path.push(Pos2 { - x: pos.x - response.rect.min.x, - y: pos.y - response.rect.min.y, - }), - None => (), - }, - // Delete cursor position (slices vector) - CursorMode::Delete => match sl_idx { - Some(idx) => drop(path.remove(idx)), // Deletes the elements - None => (), - }, - CursorMode::Trim => match sl_idx { - Some(idx) => drop(path.drain(idx..)), // Deletes elements from idx - None => (), - }, - } - } - // Handle drags - Edit mode only - if *mode == CursorMode::Edit && path.len() > 0 { - // Set selected at drag start - if response.drag_started() { - // Drag started, set current index as selected. - // This is to prevent, say, dragging over another point from stealing focus from - // the currently selected point. - match sl_idx { - Some(idx) => *selected = idx, - None => (), - } - } - // Move the selected point - if response.dragged() { - match ctx.pointer_interact_pos() { - Some(pos) => { - path[*selected] = Pos2 { - x: pos.x - response.rect.min.x, - y: pos.y - response.rect.min.y, - } - } - None => (), - } - } - } - }); - - match result { - Some(code) => { - egui::Window::new("Generated Code").show(ctx, |ui| { - egui::ScrollArea::vertical().show(ui, |ui| { - ui.code_editor(code); - }); - }); - } - None => (), - } - } -} diff --git a/src/gui.rs b/src/gui.rs new file mode 100644 index 0000000..7c3adff --- /dev/null +++ b/src/gui.rs @@ -0,0 +1,364 @@ +/* + * PATHY - A tool for creating complex paths + * in autonomous code without having to manually write + * each and every variable. + * + * Created by Daksh Gupta. + */ +pub use crate::app::PathyApp; +use crate::app::*; +use eframe::egui::Visuals; +use egui::{pos2, Color32, Pos2, Rect, Sense, Stroke, Vec2}; +use egui_extras::RetainedImage; +// Uncomment this section to get access to the console_log macro +// Use console_log to print things to console. println macro doesn't work +// here, so you'll need it. +/*use wasm_bindgen::prelude::*; +#[wasm_bindgen] +extern "C" { + // Use `js_namespace` here to bind `console.log(..)` instead of just + // `log(..)` + #[wasm_bindgen(js_namespace = console)] + fn log(s: &str); + + // The `console.log` is quite polymorphic, so we can bind it with multiple + // signatures. Note that we need to use `js_name` to ensure we always call + // `log` in JS. + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_u32(a: u32); + + // Multiple arguments too! + #[wasm_bindgen(js_namespace = console, js_name = log)] + fn log_many(a: &str, b: &str); +} +macro_rules! console_log { + // Note that this is using the `log` function imported above during + // `bare_bones` + ($($t:tt)*) => (log(&format_args!($($t)*).to_string())) +} +// */ +/// We derive Deserialize/Serialize so we can persist app state on shutdown. + +impl eframe::App for PathyApp { + /// Called by the frame work to save state before shutdown. + fn save(&mut self, storage: &mut dyn eframe::Storage) { + eframe::set_value(storage, eframe::APP_KEY, self); + } + + /// Called each time the UI needs repainting, which may be many times per second. + /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // Dark mode + ctx.set_visuals(Visuals::dark()); + let Self { + height, + width, + scale, + mode, + path, + selected, + overlay, + result, + processed, + } = self; + + #[cfg(not(target_arch = "wasm32"))] // no File->Quit on web pages! + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + // The top panel is often a good place for a menu bar: + egui::menu::bar(ui, |ui| { + ui.menu_button("File", |ui| { + if ui.button("Quit").clicked() { + _frame.close(); + } + }); + }); + }); + + egui::SidePanel::left("side_panel").show(ctx, |ui| { + ui.heading("Settings"); + + // Size settings + let response = ui.add_enabled_ui(path.len() == 0, |ui| { + ui.label("Field Dimensions:"); + ui.horizontal(|ui| { + ui.add(egui::DragValue::new(height)); + ui.label("Height (Inches)") + }); + ui.horizontal(|ui| { + ui.add(egui::DragValue::new(width)); + ui.label("Width (Inches)") + }); + ui.add(egui::Slider::new(scale, 0.0..=1000.0).text("Scale")); + }); + if path.len() != 0 { + response.response.on_hover_text_at_pointer( + "Size settings may not be changed once you've created a path.", + ); + } + ui.label("Drop an image to set an overlay!"); + + // Notice + ui.with_layout(egui::Layout::bottom_up(egui::Align::LEFT), |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("Made for "); + ui.hyperlink_to("EZTemplate", "https://ez-robotics.github.io/EZ-Template/"); + ui.label(" and "); + ui.hyperlink_to("PROS", "https://pros.cs.purdue.edu"); + ui.label("."); + }); + }); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + // The central panel the region left after adding TopPanel's and SidePanel's + + ui.heading("Pathy"); + ui.label("Created by Daksh Gupta."); + ui.label("Made for use in auton code. Generated code uses the EZTemplate API."); + egui::warn_if_debug_build(ui); + }); + + // Path Designer + egui::Window::new("Path Designer").show(ctx, |ui| { + // If the width is scale, find the height that keeps it + // in the correct aspect ratio + let aspecty: f32 = (*height / *width) * *scale; + ui.heading("Path Designer"); + // UI Buttons + ui.horizontal(|ui| { + if ui.button("Create").clicked() { + *mode = CursorMode::Create; + // Reset processes + *processed = Vec::new(); + } + if ui.button("Edit").clicked() { + *mode = CursorMode::Edit; + *processed = Vec::new(); + } + if ui.button("Delete").clicked() { + *mode = CursorMode::Delete; + *processed = Vec::new(); + } + if ui.button("Trim").clicked() { + *mode = CursorMode::Trim; + *processed = Vec::new(); + } + if ui.button("Clear").clicked() { + *path = Vec::new(); // Clear path + *processed = Vec::new(); + *result = None; + } + if ui.button("Preprocess").clicked() { + *processed = Self::preprocess(path, (*scale, aspecty), (*width, *height)); + } + // Order here is important, as the ui button is only rendered if the first + // condition is true. Otherwise, there's no point in evaluating the second + // condition, thus not rendering the button. + if processed.len() != 0 && ui.button("Generate").clicked() { + *result = Some(Self::generate(processed)); + } + if *mode != CursorMode::Default && ui.button("Finish").clicked() { + *mode = CursorMode::Default; + } + }); + ui.add_space(10.0); + // Render the bounds + let (rect, response) = ui.allocate_at_least( + Vec2 { + x: *scale, + y: aspecty, + }, + Sense::click_and_drag(), + ); + ui.painter().rect( + rect, + 0.0, + Color32::from_gray(64), + Stroke::new(5.0, Color32::WHITE), + ); + // Check for dropped files + ctx.input(|i| match i.raw.dropped_files.last() { + Some(file) => match file.clone().bytes { + Some(bytes) => match RetainedImage::from_image_bytes("", &*bytes) { + Ok(image) => *overlay = Some(image), + Err(_) => (), + }, + None => (), + }, + None => (), + }); + // Render image + match overlay { + Some(image) => { + //image.show_scaled(ui, *scale / (image.width() as f32)); + ui.painter().image( + image.texture_id(ctx), + rect, + Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), + Color32::WHITE, + ); + } + None => (), + } + // Variable we'll use to render lines + let mut prev: Option = None; + // Line stroke + let yellow_line = Stroke { + width: 3.0, + color: Color32::YELLOW, + }; + // Render all lines + path.iter().for_each(|pos| { + let screen_pos = Pos2 { + x: response.rect.min.x + pos.x, + y: response.rect.min.y + pos.y, + }; + // Render lines + match prev { + Some(prev_pos) => { + ui.painter() + .line_segment([prev_pos, screen_pos], yellow_line); + } + None => (), + }; + prev = Some(screen_pos); + }); + // Render all points + path.iter().enumerate().for_each(|(idx, pos)| { + let screen_pos = Pos2 { + x: response.rect.min.x + pos.x, + y: response.rect.min.y + pos.y, + }; + // Render points + if idx == 0 { + ui.painter().circle_filled(screen_pos, 5.0, Color32::GREEN); + } else if idx == path.len() - 1 { + ui.painter().circle_filled(screen_pos, 5.0, Color32::BLUE); + } else { + ui.painter().circle_filled(screen_pos, 5.0, Color32::YELLOW); + } + }); + // Hovered point, Edit and Delete will actually set this to be the closest point. + let mut hovered = response.hover_pos(); + // Index of selected point of path (used by edit & delete) + let mut sl_idx: Option = None; + // Render the tooltips + match *mode { + CursorMode::Default => (), // No tooltip + CursorMode::Create => { + // Get pointer position + match hovered { + // Put circle under cursor + Some(pos) => { + ui.painter().circle_filled(pos, 5.0, Color32::YELLOW); + // Render line preview + match prev { + Some(prev_pos) => { + ui.painter().line_segment([prev_pos, pos], yellow_line) + } + None => (), + } + } + None => (), + } + } + CursorMode::Edit | CursorMode::Delete | CursorMode::Trim => { + // Get pointer position + match hovered { + Some(hover_pos) => { + // Find the nearest point. We just add the x and y differences without + // squaring them, since we don't need the actual distance, just + // something we can compare (and works 99% of the time). + let mut distance = f32::MAX; + hovered = Some(path.iter().enumerate().fold( + hover_pos, + |old_pos, (idx, pos)| { + let screen_pos = Pos2 { + x: response.rect.min.x + pos.x, + y: response.rect.min.y + pos.y, + }; + let dis = f32::abs(hover_pos.x - screen_pos.x) + + f32::abs(hover_pos.y - screen_pos.y); + if dis < distance { + distance = dis; + sl_idx = Some(idx); + return screen_pos; + } + old_pos + }, + )); + // Render closest point red + ui.painter() + .circle_filled(hovered.unwrap(), 5.1, Color32::RED); + } + None => (), + } + } + } + // Handle clicks + // I'd like to do this first, so there's no frame delay, but it's a little more + // idiomatic for me to do it this way, since we can now use a match statement above + // (since Delete mode uses the nearest point calculated for the tooltip). + if response.clicked() { + match mode { + // Default does nothing, and Edit uses drags + CursorMode::Default | CursorMode::Edit => (), + // Add cursor position to list + CursorMode::Create => match ctx.pointer_interact_pos() { + Some(pos) => path.push(Pos2 { + x: pos.x - response.rect.min.x, + y: pos.y - response.rect.min.y, + }), + None => (), + }, + // Delete cursor position (slices vector) + CursorMode::Delete => match sl_idx { + Some(idx) => drop(path.remove(idx)), // Deletes the elements + None => (), + }, + CursorMode::Trim => match sl_idx { + Some(idx) => drop(path.drain(idx..)), // Deletes elements from idx + None => (), + }, + } + } + // Handle drags - Edit mode only + if *mode == CursorMode::Edit && path.len() > 0 { + // Set selected at drag start + if response.drag_started() { + // Drag started, set current index as selected. + // This is to prevent, say, dragging over another point from stealing focus from + // the currently selected point. + match sl_idx { + Some(idx) => *selected = idx, + None => (), + } + } + // Move the selected point + if response.dragged() { + match ctx.pointer_interact_pos() { + Some(pos) => { + path[*selected] = Pos2 { + x: pos.x - response.rect.min.x, + y: pos.y - response.rect.min.y, + } + } + None => (), + } + } + } + }); + + match result { + Some(code) => { + egui::Window::new("Generated Code").show(ctx, |ui| { + egui::ScrollArea::vertical().show(ui, |ui| { + ui.code_editor(code); + }); + }); + } + None => (), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 67d40d7..d3187d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,6 @@ #![warn(clippy::all, rust_2018_idioms)] mod app; -pub use app::PathyApp; +mod gui; + +pub use gui::PathyApp;