refactor(topola-egui): Move InteractiveEvent handling from Viewport into Workspace

This commit is contained in:
Ellen Emilia Anna Zscheile 2025-06-27 06:50:01 +02:00
parent c66089bca9
commit c8848ef269
5 changed files with 159 additions and 117 deletions

View File

@ -131,13 +131,14 @@ impl Overlay {
_board: &Board<impl AccessMesadata>, _board: &Board<impl AccessMesadata>,
_appearance_panel: &AppearancePanel, _appearance_panel: &AppearancePanel,
at: Point, at: Point,
modifiers: &egui::Modifiers, ctrl: bool,
shift: bool,
) { ) {
if self.reselect_bbox.is_none() { if self.reselect_bbox.is_none() {
// handle bounding box selection // handle bounding box selection
let selmode = if modifiers.ctrl { let selmode = if ctrl {
SelectionMode::Toggling SelectionMode::Toggling
} else if modifiers.shift { } else if shift {
SelectionMode::Addition SelectionMode::Addition
} else { } else {
SelectionMode::Substitution SelectionMode::Substitution

View File

@ -2,7 +2,6 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
use core::ops::ControlFlow;
use geo::point; use geo::point;
use petgraph::{ use petgraph::{
data::DataMap, data::DataMap,
@ -10,12 +9,8 @@ use petgraph::{
}; };
use rstar::{Envelope, AABB}; use rstar::{Envelope, AABB};
use topola::{ use topola::{
autorouter::{ autorouter::invoker::{
execution::Command, GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts, GetObstacles,
invoker::{
GetGhosts, GetMaybeNavcord, GetMaybeThetastarStepper, GetNavmeshDebugTexts,
GetObstacles,
},
}, },
board::AccessMesadata, board::AccessMesadata,
drawing::{ drawing::{
@ -25,10 +20,10 @@ use topola::{
geometry::{shape::AccessShape, GenericNode}, geometry::{shape::AccessShape, GenericNode},
graph::MakeRef, graph::MakeRef,
interactor::{ interactor::{
activity::{ActivityStepper, InteractiveEvent, InteractiveInput}, activity::{ActivityStepper, InteractiveEvent, InteractiveEventKind, InteractiveInput},
interaction::InteractionStepper, interaction::InteractionStepper,
}, },
layout::{poly::MakePolygon, via::ViaWeight}, layout::poly::MakePolygon,
math::{Circle, RotationSense}, math::{Circle, RotationSense},
router::navmesh::NavnodeIndex, router::navmesh::NavnodeIndex,
}; };
@ -138,95 +133,52 @@ impl Viewport {
if let Some(workspace) = maybe_workspace { if let Some(workspace) = maybe_workspace {
let latest_point = point! {x: latest_pos.x as f64, y: -latest_pos.y as f64}; let latest_point = point! {x: latest_pos.x as f64, y: -latest_pos.y as f64};
// Advances the app's state by the delta time `dt`. May call let interactive_input = InteractiveInput {
// `.update_state()` more than once if the delta time is more than a multiple of active_layer: workspace.appearance_panel.active_layer,
// the timestep. pointer_pos: latest_point,
let dt = ctx.input(|i| i.stable_dt); dt: ctx.input(|i| i.stable_dt),
let active_layer = workspace.appearance_panel.active_layer; };
self.update_counter += dt; workspace.advance_state_by_dt(
while self.update_counter >= menu_bar.frame_timestep { tr,
self.update_counter -= menu_bar.frame_timestep; error_dialog,
if let ControlFlow::Break(()) = workspace.update_state( menu_bar.frame_timestep,
tr, &interactive_input,
error_dialog, );
&InteractiveInput {
active_layer,
pointer_pos: point! {x: latest_pos.x as f64, y: latest_pos.y as f64},
dt,
},
) {
break;
}
}
if !workspace.interactor.maybe_activity().as_ref().map_or(true, |activity| { let interactive_event_kind =
matches!(activity.maybe_status(), Some(ControlFlow::Break(..))) if response.clicked_by(egui::PointerButton::Primary) {
}) { Some(InteractiveEventKind::PointerPrimaryButtonClicked)
// there is currently some activity } else if response.drag_started_by(egui::PointerButton::Primary) {
let interactive_event = if response.clicked_by(egui::PointerButton::Primary) { Some(InteractiveEventKind::PointerPrimaryButtonDragStarted)
Some(InteractiveEvent::PointerPrimaryButtonClicked) } else if response.drag_stopped_by(egui::PointerButton::Primary) {
Some(InteractiveEventKind::PointerPrimaryButtonDragStopped)
} else if response.clicked_by(egui::PointerButton::Secondary) { } else if response.clicked_by(egui::PointerButton::Secondary) {
Some(InteractiveEvent::PointerSecondaryButtonClicked) Some(InteractiveEventKind::PointerSecondaryButtonClicked)
} else { } else {
None None
}; };
if let Some(interactive_event) = interactive_event { if let Some(kind) = interactive_event_kind {
let dt = ctx.input(|i| i.stable_dt); let (ctrl, shift) = response
let active_layer = workspace.appearance_panel.active_layer; .ctx
let _ = workspace.update_state_for_event( .input(|i| (i.modifiers.ctrl, i.modifiers.shift));
tr, let _ = workspace.update_state_for_event(
error_dialog, tr,
&InteractiveInput { error_dialog,
active_layer, menu_bar,
pointer_pos: latest_point, &interactive_input,
dt, InteractiveEvent { kind, ctrl, shift },
}, );
interactive_event, } else if let Some((_, bsk, cur_bbox)) =
); workspace.overlay.get_bbox_reselect(latest_point)
} {
} else { use topola::autorouter::selection::BboxSelectionKind;
let layers = &mut workspace.appearance_panel; painter.paint_bbox_with_color(
let overlay = &mut workspace.overlay; cur_bbox,
let board = workspace.interactor.invoker().autorouter().board(); match bsk {
if response.clicked_by(egui::PointerButton::Primary) { BboxSelectionKind::CompletelyInside => egui::Color32::YELLOW,
if menu_bar.is_placing_via { BboxSelectionKind::MerelyIntersects => egui::Color32::BLUE,
workspace.interactor.execute(Command::PlaceVia(ViaWeight { },
from_layer: 0, );
to_layer: 0,
circle: Circle {
pos: latest_point,
r: menu_bar
.autorouter_options
.router_options
.routed_band_width
/ 2.0,
},
maybe_net: Some(1234),
}));
} else {
overlay.click(board, layers, latest_point);
}
} else if response.drag_started_by(egui::PointerButton::Primary) {
overlay.drag_start(
board,
layers,
latest_point,
&response.ctx.input(|i| i.modifiers),
);
} else if response.drag_stopped_by(egui::PointerButton::Primary) {
overlay.drag_stop(board, layers, latest_point);
} else if let Some((_, bsk, cur_bbox)) =
overlay.get_bbox_reselect(latest_point)
{
use topola::autorouter::selection::BboxSelectionKind;
painter.paint_bbox_with_color(
cur_bbox,
match bsk {
BboxSelectionKind::CompletelyInside => egui::Color32::YELLOW,
BboxSelectionKind::MerelyIntersects => egui::Color32::BLUE,
},
);
}
} }
let layers = &mut workspace.appearance_panel; let layers = &mut workspace.appearance_panel;
@ -529,8 +481,13 @@ impl Viewport {
.paint_primitive(ghost, egui::Color32::from_rgb(75, 75, 150)); .paint_primitive(ghost, egui::Color32::from_rgb(75, 75, 150));
} }
if let ActivityStepper::Interaction(InteractionStepper::RoutePlan(rp)) = activity.activity() { if let ActivityStepper::Interaction(InteractionStepper::RoutePlan(rp)) =
painter.paint_linestring(&rp.lines, egui::Color32::from_rgb(245, 182, 66)); activity.activity()
{
painter.paint_linestring(
&rp.lines,
egui::Color32::from_rgb(245, 182, 66),
);
} }
if let Some(ref navmesh) = if let Some(ref navmesh) =

View File

@ -8,18 +8,19 @@ use std::{
}; };
use topola::{ use topola::{
autorouter::history::History, autorouter::{execution::Command, history::History},
interactor::{ interactor::{
activity::{InteractiveEvent, InteractiveInput}, activity::{InteractiveEvent, InteractiveEventKind, InteractiveInput},
Interactor, Interactor,
}, },
layout::LayoutEdit, layout::{via::ViaWeight, LayoutEdit},
math::Circle,
specctra::{design::SpecctraDesign, mesadata::SpecctraMesadata}, specctra::{design::SpecctraDesign, mesadata::SpecctraMesadata},
}; };
use crate::{ use crate::{
appearance_panel::AppearancePanel, error_dialog::ErrorDialog, overlay::Overlay, appearance_panel::AppearancePanel, error_dialog::ErrorDialog, menu_bar::MenuBar,
translator::Translator, overlay::Overlay, translator::Translator,
}; };
/// A loaded design and associated structures. /// A loaded design and associated structures.
@ -33,6 +34,8 @@ pub struct Workspace {
Sender<std::io::Result<Result<History, serde_json::Error>>>, Sender<std::io::Result<Result<History, serde_json::Error>>>,
Receiver<std::io::Result<Result<History, serde_json::Error>>>, Receiver<std::io::Result<Result<History, serde_json::Error>>>,
), ),
update_counter: f32,
} }
impl Workspace { impl Workspace {
@ -58,25 +61,94 @@ impl Workspace {
) )
})?, })?,
history_channel: channel(), history_channel: channel(),
update_counter: 0.0,
}) })
} }
/// Advances the app's state by the delta time `dt`. May call
/// `.update_state()` more than once if the delta time is more than a multiple of
/// the timestep.
pub fn advance_state_by_dt(
&mut self,
tr: &Translator,
error_dialog: &mut ErrorDialog,
frame_timestep: f32,
interactive_input: &InteractiveInput,
) {
self.update_counter += interactive_input.dt;
while self.update_counter >= frame_timestep {
self.update_counter -= frame_timestep;
if let ControlFlow::Break(()) = self.update_state(tr, error_dialog, interactive_input) {
break;
}
}
}
pub fn update_state_for_event( pub fn update_state_for_event(
&mut self, &mut self,
tr: &Translator, tr: &Translator,
error_dialog: &mut ErrorDialog, error_dialog: &mut ErrorDialog,
menu_bar: &MenuBar,
interactive_input: &InteractiveInput, interactive_input: &InteractiveInput,
interactive_event: InteractiveEvent, interactive_event: InteractiveEvent,
) -> ControlFlow<()> { ) {
match self if !self
.interactor .interactor
.update_for_event(interactive_input, interactive_event) .maybe_activity()
.as_ref()
.map_or(true, |activity| {
matches!(activity.maybe_status(), Some(ControlFlow::Break(..)))
})
{ {
ControlFlow::Continue(()) => ControlFlow::Continue(()), match self
ControlFlow::Break(Ok(())) => ControlFlow::Break(()), .interactor
ControlFlow::Break(Err(err)) => { .update_for_event(interactive_input, interactive_event)
error_dialog.push_error("tr-module-invoker", format!("{}", err)); {
ControlFlow::Break(()) ControlFlow::Continue(()) | ControlFlow::Break(Ok(())) => {}
ControlFlow::Break(Err(err)) => {
error_dialog.push_error("tr-module-invoker", format!("{}", err));
}
}
} else {
let board = self.interactor.invoker().autorouter().board();
match interactive_event.kind {
InteractiveEventKind::PointerPrimaryButtonClicked => {
if menu_bar.is_placing_via {
self.interactor.execute(Command::PlaceVia(ViaWeight {
from_layer: 0,
to_layer: 0,
circle: Circle {
pos: interactive_input.pointer_pos,
r: menu_bar.autorouter_options.router_options.routed_band_width
/ 2.0,
},
maybe_net: Some(1234),
}));
} else {
self.overlay.click(
board,
&self.appearance_panel,
interactive_input.pointer_pos,
);
}
}
InteractiveEventKind::PointerPrimaryButtonDragStarted => {
self.overlay.drag_start(
board,
&self.appearance_panel,
interactive_input.pointer_pos,
interactive_event.ctrl,
interactive_event.shift,
);
}
InteractiveEventKind::PointerPrimaryButtonDragStopped => {
self.overlay.drag_stop(
board,
&self.appearance_panel,
interactive_input.pointer_pos,
);
}
_ => {}
} }
} }
} }

View File

@ -35,13 +35,25 @@ pub struct InteractiveInput {
pub dt: f32, pub dt: f32,
} }
/// An event received from the user
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum InteractiveEvent { pub enum InteractiveEventKind {
PointerPrimaryButtonClicked, PointerPrimaryButtonClicked,
PointerPrimaryButtonDragStarted,
PointerPrimaryButtonDragStopped,
PointerSecondaryButtonClicked, PointerSecondaryButtonClicked,
} }
/// An event received from the user
pub struct InteractiveEvent {
pub kind: InteractiveEventKind,
/// `true` if the `Ctrl` key pressed during the event
pub ctrl: bool,
/// `true` if the `Shift` key pressed during the event
pub shift: bool,
}
/// This is the execution context passed to the stepper on each step /// This is the execution context passed to the stepper on each step
pub struct ActivityContext<'a, M> { pub struct ActivityContext<'a, M> {
pub interactive_input: &'a InteractiveInput, pub interactive_input: &'a InteractiveInput,

View File

@ -15,7 +15,7 @@ use crate::{
}; };
use super::{ use super::{
activity::{ActivityContext, InteractiveEvent}, activity::{ActivityContext, InteractiveEvent, InteractiveEventKind},
interaction::InteractionError, interaction::InteractionError,
}; };
@ -128,8 +128,8 @@ impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent> for Ro
context: &mut ActivityContext<M>, context: &mut ActivityContext<M>,
event: InteractiveEvent, event: InteractiveEvent,
) -> Result<(), InteractionError> { ) -> Result<(), InteractionError> {
match event { match event.kind {
InteractiveEvent::PointerPrimaryButtonClicked if self.end_pin.is_none() => { InteractiveEventKind::PointerPrimaryButtonClicked if self.end_pin.is_none() => {
if let Some((layer, idx, pos)) = try_select_pin(context) { if let Some((layer, idx, pos)) = try_select_pin(context) {
// make sure double-click or such doesn't corrupt state // make sure double-click or such doesn't corrupt state
if let Some(start_pin) = self.start_pin { if let Some(start_pin) = self.start_pin {
@ -151,7 +151,7 @@ impl<M: AccessMesadata> OnEvent<ActivityContext<'_, M>, InteractiveEvent> for Ro
} }
} }
} }
InteractiveEvent::PointerSecondaryButtonClicked => { InteractiveEventKind::PointerSecondaryButtonClicked => {
if self.end_pin.is_some() { if self.end_pin.is_some() {
log::debug!("un-clicked end pin"); log::debug!("un-clicked end pin");
self.end_pin = None; self.end_pin = None;