diff --git a/src/ui/panes/pid_drawing_tool.rs b/src/ui/panes/pid_drawing_tool.rs index c704868f7a9c7e4704d20d9305945e7c058f4ff0..b9113ccec8f948819120c51aff3253a132b0f1ae 100644 --- a/src/ui/panes/pid_drawing_tool.rs +++ b/src/ui/panes/pid_drawing_tool.rs @@ -4,7 +4,7 @@ mod pos; mod symbols; use connections::Connection; -use egui::{epaint::PathStroke, Color32, CursorIcon, PointerButton, Pos2, Sense, Theme, Vec2}; +use egui::{epaint::PathStroke, Color32, Context, PointerButton, Pos2, Sense, Theme, Ui, Vec2}; use elements::Element; use pos::Pos; use serde::{Deserialize, Serialize}; @@ -16,50 +16,161 @@ use crate::ui::composable_view::PaneResponse; use super::PaneBehavior; +#[derive(Clone, Serialize, Deserialize, PartialEq)] +enum Action { + Connect(usize), + ContextMenu(Pos), + Drag(usize), +} + /// Piping and instrumentation diagram -#[derive(Default, Clone, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Serialize, Deserialize, PartialEq)] pub struct PidPane { elements: Vec<Element>, connections: Vec<Connection>, - /// Index of the element the drag operation started on - dragged: Option<usize>, + grid_size: f32, + + #[serde(skip)] + action: Option<Action>, +} - /// Index of the element the connection operation started on - connect_element: Option<usize>, +impl Default for PidPane { + fn default() -> Self { + Self { + elements: Vec::default(), + connections: Vec::default(), + grid_size: 10.0, - context_menu_pos: Pos, + action: None, + } + } } impl PaneBehavior for PidPane { fn ui(&mut self, ui: &mut egui::Ui) -> PaneResponse { - let step_size: i32 = 10; - let window_rect = ui.max_rect(); - let painter = ui.painter(); + // Set cursor icon + // ui.ctx().output_mut(|output| output.cursor_icon = CursorIcon::Grab); + + let theme = PidPane::find_theme(ui.ctx()); + self.draw_grid(theme, ui); + self.draw_connections(ui); + self.draw_elements(theme, ui); + + // Allocate the space to sense inputs + let (_, response) = ui.allocate_at_least(ui.max_rect().size(), Sense::click_and_drag()); + let pointer_pos = response.hover_pos().map(|pos| self.screen_to_grid_pos(pos)); + + // Detect the action + if let Some(pointer_pos) = &pointer_pos { + if response.clicked_by(PointerButton::Secondary) { + println!("Context menu opened"); + self.action = Some(Action::ContextMenu(pointer_pos.clone())); + } else if response.drag_started() { + println!("Drag started"); + self.action = self + .find_hovered_element_idx(pointer_pos) + .map(|idx| Action::Drag(idx)); + } else if response.drag_stopped() { + self.action.take(); + println!("Drag stopped"); + } + } + + // Context menu + if let Some(Action::ContextMenu(pointer_pos)) = self.action.clone() { + response.context_menu(|ui| self.draw_context_menu(&pointer_pos, ui)); + } + + // Connect action + if let Some(pointer_pos) = pointer_pos { + match self.action { + Some(Action::Connect(start)) => { + if let Some(end) = self.find_hovered_element_idx(&pointer_pos) { + if response.clicked() { + if start != end { + self.connections.push(Connection { start, end }); + println!("Added connection from {} to {}", start, end); + } + self.action.take(); + println!("Connect action ended"); + } + } + } + Some(Action::Drag(idx)) => { + let element = &mut self.elements[idx]; + element.position.x = pointer_pos.x - element.size / 2; + element.position.y = pointer_pos.y - element.size / 2; + } + _ => {} + } + } + + PaneResponse::default() + } - ui.ctx() - .output_mut(|output| output.cursor_icon = CursorIcon::Grab); + fn contains_pointer(&self) -> bool { + false + } +} + +impl PidPane { + fn is_hovering_element(&self, pointer_pos: &Pos) -> bool { + self.elements + .iter() + .find(|element| element.contains(pointer_pos)) + .is_some() + } - let theme = ui.ctx().options(|options| match options.theme_preference { + /// Returns the currently used theme + fn find_theme(ctx: &Context) -> Theme { + // In Egui you can either decide a theme or use the system one. + // If the system theme cannot be determined, a fallback theme can be set. + ctx.options(|options| match options.theme_preference { egui::ThemePreference::Light => Theme::Light, egui::ThemePreference::Dark => Theme::Dark, - egui::ThemePreference::System => match ui.ctx().system_theme() { + egui::ThemePreference::System => match ctx.system_theme() { Some(Theme::Light) => Theme::Light, Some(Theme::Dark) => Theme::Dark, None => options.fallback_theme, }, - }); - - // Draw the dot grid - let dot_color = match theme { - Theme::Dark => egui::Color32::DARK_GRAY, - Theme::Light => egui::Color32::BLACK, - }; - for x in - (window_rect.min.x as i32..window_rect.max.x.round() as i32).step_by(step_size as usize) + }) + } + + fn dots_color(theme: Theme) -> Color32 { + match theme { + Theme::Dark => Color32::DARK_GRAY, + Theme::Light => Color32::BLACK, + } + } + + fn screen_to_grid_pos(&self, screen_pos: Pos2) -> Pos { + Pos { + x: (screen_pos.x / self.grid_size) as i32, + y: (screen_pos.y / self.grid_size) as i32, + } + } + + fn find_hovered_element_idx(&self, pos: &Pos) -> Option<usize> { + self.elements.iter().position(|elem| elem.contains(&pos)) + } + + fn find_hovered_element_mut(&mut self, pos: &Pos) -> Option<&mut Element> { + self.elements + .iter_mut() + .find(|element| element.contains(&pos)) + } + + fn draw_grid(&self, theme: Theme, ui: &Ui) { + let painter = ui.painter(); + let window_rect = ui.max_rect(); + let dot_color = PidPane::dots_color(theme); + + for x in (window_rect.min.x as i32..window_rect.max.x.round() as i32) + .step_by(self.grid_size as usize) { for y in (window_rect.min.y as i32..window_rect.max.y.round() as i32) - .step_by(step_size as usize) + .step_by(self.grid_size as usize) { let rect = egui::Rect::from_min_size( egui::Pos2::new(x as f32, y as f32), @@ -68,35 +179,37 @@ impl PaneBehavior for PidPane { painter.rect_filled(rect, 0.0, dot_color); } } + } + + fn draw_connections(&self, ui: &Ui) { + let painter = ui.painter(); for connection in &self.connections { let elem1 = &self.elements[connection.start]; let elem2 = &self.elements[connection.end]; - let x1 = (elem1.position.x + elem1.size / 2) * step_size; - let y1 = (elem1.position.y + elem1.size / 2) * step_size; - let x2 = (elem2.position.x + elem2.size / 2) * step_size; - let y2 = (elem2.position.y + elem2.size / 2) * step_size; + let x1 = (elem1.position.x + elem1.size / 2) as f32 * self.grid_size; + let y1 = (elem1.position.y + elem1.size / 2) as f32 * self.grid_size; + let x2 = (elem2.position.x + elem2.size / 2) as f32 * self.grid_size; + let y2 = (elem2.position.y + elem2.size / 2) as f32 * self.grid_size; painter.line_segment( - [ - Pos2::new(x1 as f32, y1 as f32), - Pos2::new(x2 as f32, y2 as f32), - ], + [Pos2::new(x1, y1), Pos2::new(x2, y2)], PathStroke::new(1.0, Color32::GREEN), ); } + } - // Draw elements + fn draw_elements(&self, theme: Theme, ui: &Ui) { for element in &self.elements { let image_rect = egui::Rect::from_min_size( egui::Pos2::new( - (element.position.x * step_size) as f32, - (element.position.y * step_size) as f32, + element.position.x as f32 * self.grid_size, + element.position.y as f32 * self.grid_size, ), egui::Vec2::new( - (element.size * step_size) as f32, - (element.size * step_size) as f32, + element.size as f32 * self.grid_size, + element.size as f32 * self.grid_size, ), ); @@ -104,118 +217,48 @@ impl PaneBehavior for PidPane { .rotate(element.rotation, Vec2::new(0.5, 0.5)) .paint_at(ui, image_rect); } + } - let (_, response) = ui.allocate_at_least(window_rect.size(), Sense::click_and_drag()); - - let pointer_pos = response - .hover_pos() - .map(|pos| Pos { - x: pos.x as i32 / step_size, - y: pos.y as i32 / step_size, - }) - .unwrap_or(Pos { x: 0, y: 0 }); + fn draw_context_menu(&mut self, pointer_pos: &Pos, ui: &mut Ui) { + ui.set_max_width(100.0); // To make sure we wrap long text - if response.clicked_by(PointerButton::Secondary) { - self.context_menu_pos = pointer_pos.clone(); - } - response.context_menu(|ui| { - ui.set_max_width(200.0); // To make sure we wrap long text - - if self.is_hovering_element(&self.context_menu_pos) { - if ui.button("Connect").clicked() { - self.connect_element = self - .elements - .iter() - .position(|element| element.contains(&self.context_menu_pos)); - ui.close_menu(); + if self.is_hovering_element(&pointer_pos) { + let hovered_element = self.find_hovered_element_idx(&pointer_pos); + if ui.button("Connect").clicked() { + if let Some(idx) = hovered_element { + println!("Connect action started"); + self.action = Some(Action::Connect(idx)); + } else { + panic!("No element found where the \"Connect\" action was issued"); } + ui.close_menu(); + } - if ui.button("Rotate 90° ⟲").clicked() { - if let Some(elem) = self - .elements - .iter_mut() - .find(|element| element.contains(&self.context_menu_pos)) - { - elem.rotation += PI / 2.0; - } - ui.close_menu(); + if ui.button("Rotate 90° ⟲").clicked() { + if let Some(elem) = self.find_hovered_element_mut(&pointer_pos) { + elem.rotation += PI / 2.0; + } else { + panic!("No element found where the \"Rotate 90° ⟲\" action was issued"); } - if ui.button("Rotate 90° ⟳").clicked() { - if let Some(elem) = self - .elements - .iter_mut() - .find(|element| element.contains(&self.context_menu_pos)) - { - elem.rotation -= PI / 2.0; - } - ui.close_menu(); + ui.close_menu(); + } + if ui.button("Rotate 90° ⟳").clicked() { + if let Some(elem) = self.find_hovered_element_mut(&pointer_pos) { + elem.rotation -= PI / 2.0; + } else { + panic!("No element found where the \"Rotate 90° ⟳\" action was issued"); } + ui.close_menu(); } - + } else { ui.menu_button("Symbols", |ui| { for symbol in Symbol::iter() { if ui.button(symbol.to_string()).clicked() { - self.elements.push(Element { - position: self.context_menu_pos.clone(), - size: 10, - rotation: 0.0, - symbol, - }); + self.elements.push(Element::new(pointer_pos, symbol)); ui.close_menu(); } } }); - }); - - if response.clicked() && self.connect_element.is_some() { - let second_connect_element = self - .elements - .iter() - .position(|element| element.contains(&pointer_pos)); - - if let (Some(elem1), Some(elem2)) = (self.connect_element, second_connect_element) { - if elem1 != elem2 { - self.connections.push(Connection { - start: elem1, - end: elem2, - }); - } - self.connect_element.take(); - } } - - if response.drag_started() { - // Find which element the drag started on - self.dragged = self - .elements - .iter() - .position(|element| element.contains(&pointer_pos)); - } - if response.dragged() { - if let Some(dragged) = self.dragged { - let element = &mut self.elements[dragged]; - - element.position.x = pointer_pos.x - element.size / 2; - element.position.y = pointer_pos.y - element.size / 2; - } - } - if response.drag_stopped() { - self.dragged.take(); - } - - PaneResponse::default() - } - - fn contains_pointer(&self) -> bool { - false - } -} - -impl PidPane { - fn is_hovering_element(&self, pointer_pos: &Pos) -> bool { - self.elements - .iter() - .find(|element| element.contains(pointer_pos)) - .is_some() } }