From 489f55a8b081994108f8fcd33ebeba09b368b488 Mon Sep 17 00:00:00 2001 From: Mikolaj Wielgus Date: Fri, 11 Oct 2024 02:50:42 +0200 Subject: [PATCH] refactor(egui): split out some activity code to new module, `interactor` --- src/bin/topola-egui/app.rs | 10 ++-- src/bin/topola-egui/interactor.rs | 90 +++++++++++++++++++++++++++++++ src/bin/topola-egui/main.rs | 1 + src/bin/topola-egui/menu_bar.rs | 37 +++++++------ src/bin/topola-egui/viewport.rs | 16 +++--- src/bin/topola-egui/workspace.rs | 61 +++++++++------------ 6 files changed, 149 insertions(+), 66 deletions(-) create mode 100644 src/bin/topola-egui/interactor.rs diff --git a/src/bin/topola-egui/app.rs b/src/bin/topola-egui/app.rs index 077bc16..9739199 100644 --- a/src/bin/topola-egui/app.rs +++ b/src/bin/topola-egui/app.rs @@ -1,6 +1,7 @@ use std::{ future::Future, io, + ops::ControlFlow, sync::mpsc::{channel, Receiver, Sender}, }; use unic_langid::{langid, LanguageIdentifier}; @@ -67,13 +68,13 @@ impl App { while self.update_counter >= self.menu_bar.frame_timestep { self.update_counter -= self.menu_bar.frame_timestep; - if !self.update_state() { + if let ControlFlow::Break(()) = self.update_state() { return; } } } - fn update_state(&mut self) -> bool { + fn update_state(&mut self) -> ControlFlow<()> { if let Ok(data) = self.content_channel.1.try_recv() { match data { Ok(design) => match Workspace::new(design, &self.translator) { @@ -113,7 +114,8 @@ impl App { if let Some(workspace) = &mut self.maybe_workspace { return workspace.update_state(&self.translator, &mut self.error_dialog); } - false + + ControlFlow::Break(()) } } @@ -141,7 +143,7 @@ impl eframe::App for App { &self.viewport, self.maybe_workspace .as_ref() - .and_then(|w| w.maybe_activity.as_ref()), + .and_then(|w| w.interactor.maybe_activity().as_ref()), ); if self.menu_bar.show_layer_manager { diff --git a/src/bin/topola-egui/interactor.rs b/src/bin/topola-egui/interactor.rs new file mode 100644 index 0000000..1df9c38 --- /dev/null +++ b/src/bin/topola-egui/interactor.rs @@ -0,0 +1,90 @@ +use std::ops::ControlFlow; + +use spade::InsertionError; +use topola::{ + autorouter::{ + execution::{Command, ExecutionStepper}, + history::History, + invoker::{Invoker, InvokerError}, + Autorouter, + }, + board::{mesadata::AccessMesadata, Board}, + stepper::{Abort, Step}, +}; + +use crate::{ + activity::{ActivityContext, ActivityStatus, ActivityStepperWithStatus}, + interaction::InteractionContext, +}; + +pub struct Interactor { + invoker: Invoker, + activity: Option, +} + +impl Interactor { + pub fn new(board: Board) -> Result { + Ok(Self { + invoker: Invoker::new(Autorouter::new(board)?), + activity: None, + }) + } + + pub fn execute(&mut self, command: Command) -> Result<(), InvokerError> { + self.invoker.execute(command) + } + + pub fn schedule(&mut self, command: Command) -> Result<(), InvokerError> { + self.activity = Some(ActivityStepperWithStatus::new_execution( + self.invoker.execute_stepper(command)?, + )); + Ok(()) + } + + pub fn undo(&mut self) -> Result<(), InvokerError> { + self.invoker.undo() + } + + pub fn redo(&mut self) -> Result<(), InvokerError> { + self.invoker.redo() + } + + pub fn abort(&mut self) { + if let Some(ref mut activity) = self.activity { + activity.abort(&mut ActivityContext { + interaction: InteractionContext {}, + invoker: &mut self.invoker, + }); + } + } + + pub fn replay(&mut self, history: History) { + self.invoker.replay(history); + } + + pub fn update(&mut self) -> ControlFlow<()> { + if let Some(ref mut activity) = self.activity { + return match activity.step(&mut ActivityContext { + interaction: InteractionContext {}, + invoker: &mut self.invoker, + }) { + Ok(ActivityStatus::Running) => ControlFlow::Continue(()), + Ok(ActivityStatus::Finished(..)) => ControlFlow::Break(()), + Err(err) => { + //error_dialog.push_error("tr-module-invoker", format!("{}", err)); + self.activity = None; + ControlFlow::Break(()) + } + }; + } + ControlFlow::Break(()) + } + + pub fn invoker(&self) -> &Invoker { + &self.invoker + } + + pub fn maybe_activity(&self) -> &Option { + &self.activity + } +} diff --git a/src/bin/topola-egui/main.rs b/src/bin/topola-egui/main.rs index 240d48e..89025a4 100644 --- a/src/bin/topola-egui/main.rs +++ b/src/bin/topola-egui/main.rs @@ -6,6 +6,7 @@ mod app; mod config; mod error_dialog; mod interaction; +mod interactor; mod layers; mod menu_bar; mod overlay; diff --git a/src/bin/topola-egui/menu_bar.rs b/src/bin/topola-egui/menu_bar.rs index 6ef99ea..2fcd088 100644 --- a/src/bin/topola-egui/menu_bar.rs +++ b/src/bin/topola-egui/menu_bar.rs @@ -125,9 +125,13 @@ impl MenuBar { )); let workspace_activities_enabled = match &maybe_workspace { - Some(w) => w.maybe_activity.as_ref().map_or(true, |activity| { - matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) - }), + Some(w) => w + .interactor + .maybe_activity() + .as_ref() + .map_or(true, |activity| { + matches!(activity.maybe_status(), Some(ActivityStatus::Finished(..))) + }), None => false, }; @@ -277,7 +281,7 @@ impl MenuBar { } else if let Some(workspace) = maybe_workspace { if export_session.consume_key_triggered(ctx, ui) { let ctx = ui.ctx().clone(); - let board = workspace.invoker.autorouter().board(); + let board = workspace.interactor.invoker().autorouter().board(); // FIXME: I don't know how to avoid buffering the entire exported file let mut writebuf = vec![]; @@ -325,7 +329,10 @@ impl MenuBar { // FIXME: I don't think we should be buffering everything in a `Vec`. let mut writebuf = vec![]; - serde_json::to_writer_pretty(&mut writebuf, workspace.invoker.history()); + serde_json::to_writer_pretty( + &mut writebuf, + workspace.interactor.invoker().history(), + ); execute(async move { if let Some(file_handle) = task.await { @@ -334,26 +341,18 @@ impl MenuBar { } }); } else if undo.consume_key_triggered(ctx, ui) { - workspace.invoker.undo(); + workspace.interactor.undo(); } else if redo.consume_key_triggered(ctx, ui) { - workspace.invoker.redo(); + workspace.interactor.redo(); } else if abort.consume_key_triggered(ctx, ui) { - if let Some(activity) = &mut workspace.maybe_activity { - activity.abort(&mut ActivityContext { - interaction: InteractionContext {}, - invoker: &mut workspace.invoker, - }); - } + workspace.interactor.abort() } else if place_via.consume_key_enabled(ctx, ui, &mut self.is_placing_via) { } else if workspace_activities_enabled { let mut schedule = |op: fn(Selection, AutorouterOptions) -> Command| { let selection = workspace.overlay.take_selection(); - workspace.maybe_activity = - Some(ActivityStepperWithStatus::new_execution( - workspace - .invoker - .execute_stepper(op(selection, self.autorouter_options))?, - )); + workspace + .interactor + .schedule(op(selection, self.autorouter_options)); Ok::<(), InvokerError>(()) }; if remove_bands.consume_key_triggered(ctx, ui) { diff --git a/src/bin/topola-egui/viewport.rs b/src/bin/topola-egui/viewport.rs index afa4950..daa0bf3 100644 --- a/src/bin/topola-egui/viewport.rs +++ b/src/bin/topola-egui/viewport.rs @@ -56,13 +56,12 @@ impl Viewport { let mut painter = Painter::new(ui, self.transform, top.show_bboxes); if let Some(workspace) = maybe_workspace { - let invoker = &mut workspace.invoker; let layers = &mut workspace.layers; let overlay = &mut workspace.overlay; if ctx.input(|i| i.pointer.any_click()) { if top.is_placing_via { - invoker.execute( + workspace.interactor.execute( Command::PlaceVia(ViaWeight { from_layer: 0, to_layer: 0, @@ -75,13 +74,14 @@ impl Viewport { ); } else { overlay.click( - invoker.autorouter().board(), + workspace.interactor.invoker().autorouter().board(), point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}, ); } } - let board = invoker.autorouter().board(); + let board = workspace.interactor.invoker().autorouter().board(); + for i in (0..layers.visible.len()).rev() { if layers.visible[i] { for primitive in board.layout().drawing().layer_primitive_nodes(i) { @@ -92,7 +92,7 @@ impl Viewport { .contains_node(board, GenericNode::Primitive(primitive)) { layers.highlight_colors[i] - } else if let Some(activity) = &mut workspace.maybe_activity { + } else if let Some(activity) = &mut workspace.interactor.maybe_activity() { if activity.obstacles().contains(&primitive) { layers.highlight_colors[i] } else { @@ -141,7 +141,7 @@ impl Viewport { } if top.show_navmesh { - if let Some(activity) = &mut workspace.maybe_activity { + if let Some(activity) = workspace.interactor.maybe_activity() { if let Some(navmesh) = activity.maybe_navmesh() { for edge in navmesh.edge_references() { let mut from = PrimitiveIndex::from(navmesh.node_weight(edge.source()).unwrap().node) @@ -206,7 +206,7 @@ impl Viewport { painter.paint_bbox(root_bbox); } - if let Some(activity) = &mut workspace.maybe_activity { + if let Some(activity) = &mut workspace.interactor.maybe_activity() { for ghost in activity.ghosts().iter() { painter.paint_primitive(&ghost, egui::Color32::from_rgb(75, 75, 150)); } @@ -233,7 +233,7 @@ impl Viewport { } if self.scheduled_zoom_to_fit { - let root_bbox = invoker + let root_bbox = workspace.interactor.invoker() .autorouter() .board() .layout() diff --git a/src/bin/topola-egui/workspace.rs b/src/bin/topola-egui/workspace.rs index fb1d051..3ac16ed 100644 --- a/src/bin/topola-egui/workspace.rs +++ b/src/bin/topola-egui/workspace.rs @@ -1,4 +1,7 @@ -use std::sync::mpsc::{channel, Receiver, Sender}; +use std::{ + ops::ControlFlow, + sync::mpsc::{channel, Receiver, Sender}, +}; use topola::{ autorouter::{history::History, invoker::Invoker, Autorouter}, @@ -10,6 +13,7 @@ use crate::{ activity::{ActivityContext, ActivityStatus, ActivityStepperWithStatus}, error_dialog::ErrorDialog, interaction::InteractionContext, + interactor::Interactor, layers::Layers, overlay::Overlay, translator::Translator, @@ -20,9 +24,7 @@ pub struct Workspace { pub design: SpecctraDesign, pub layers: Layers, pub overlay: Overlay, - pub invoker: Invoker, - - pub maybe_activity: Option, + pub interactor: Interactor, pub history_channel: ( Sender>>, @@ -33,18 +35,11 @@ pub struct Workspace { impl Workspace { pub fn new(design: SpecctraDesign, tr: &Translator) -> Result { let board = design.make_board(); + let layers = Layers::new(&board); let overlay = Overlay::new(&board).map_err(|err| { format!( "{}; {}", - tr.text("tr-error_unable-to-initialize-overlay"), - err - ) - })?; - let layers = Layers::new(&board); - let autorouter = Autorouter::new(board).map_err(|err| { - format!( - "{}; {}", - tr.text("tr-error_unable-to-initialize-autorouter"), + tr.text("tr-error-unable-to-initialize-overlay"), err ) })?; @@ -52,24 +47,33 @@ impl Workspace { design, layers, overlay, - invoker: Invoker::new(autorouter), - maybe_activity: None, + interactor: Interactor::new(board).map_err(|err| { + format!( + "{}; {}", + tr.text("tr-error_unable-to-initialize-overlay"), + err + ) + })?, history_channel: channel(), }) } - pub fn update_state(&mut self, tr: &Translator, error_dialog: &mut ErrorDialog) -> bool { + pub fn update_state( + &mut self, + tr: &Translator, + error_dialog: &mut ErrorDialog, + ) -> ControlFlow<()> { if let Ok(data) = self.history_channel.1.try_recv() { match data { Ok(Ok(data)) => { - self.invoker.replay(data); + self.interactor.replay(data); } Ok(Err(err)) => { error_dialog.push_error( "tr-module-history-file-loader", format!( "{}; {}", - tr.text("tr-error_failed-to-parse-as-history-json"), + tr.text("tr-error-failed-to-parse-as-history-json"), err ), ); @@ -77,30 +81,17 @@ impl Workspace { Err(err) => { error_dialog.push_error( "tr-module-history-file-loader", - format!("{}; {}", tr.text("tr-error_unable-to-read-file"), err), + format!("{}; {}", tr.text("tr-error-unable-to-read-file"), err), ); } } } - if let Some(activity) = &mut self.maybe_activity { - return match activity.step(&mut ActivityContext { - interaction: InteractionContext {}, - invoker: &mut self.invoker, - }) { - Ok(ActivityStatus::Running) => true, - Ok(ActivityStatus::Finished(..)) => false, - Err(err) => { - error_dialog.push_error("tr-module-invoker", format!("{}", err)); - self.maybe_activity = None; - false - } - }; - } - false + self.interactor.update() } pub fn update_layers(&mut self, ctx: &egui::Context) { - self.layers.update(ctx, self.invoker.autorouter().board()); + self.layers + .update(ctx, self.interactor.invoker().autorouter().board()); } }