refactor(egui): split out some activity code to new module, `interactor`

This commit is contained in:
Mikolaj Wielgus 2024-10-11 02:50:42 +02:00
parent 3e9e3c69c9
commit 489f55a8b0
6 changed files with 149 additions and 66 deletions

View File

@ -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 {

View File

@ -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<M: AccessMesadata> {
invoker: Invoker<M>,
activity: Option<ActivityStepperWithStatus>,
}
impl<M: AccessMesadata> Interactor<M> {
pub fn new(board: Board<M>) -> Result<Self, InsertionError> {
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<M> {
&self.invoker
}
pub fn maybe_activity(&self) -> &Option<ActivityStepperWithStatus> {
&self.activity
}
}

View File

@ -6,6 +6,7 @@ mod app;
mod config;
mod error_dialog;
mod interaction;
mod interactor;
mod layers;
mod menu_bar;
mod overlay;

View File

@ -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<u8>`.
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) {

View File

@ -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()

View File

@ -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<SpecctraMesadata>,
pub maybe_activity: Option<ActivityStepperWithStatus>,
pub interactor: Interactor<SpecctraMesadata>,
pub history_channel: (
Sender<std::io::Result<Result<History, serde_json::Error>>>,
@ -33,18 +35,11 @@ pub struct Workspace {
impl Workspace {
pub fn new(design: SpecctraDesign, tr: &Translator) -> Result<Self, String> {
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());
}
}