1use std::time::Duration;
16
17use egui::Pos2;
18
19use crate::doc::Writability;
20use crate::gesture::Gesture;
21use blockworx_doc::document::DocIndex;
22
23use crate::canvas::painter::Painter;
24use crate::canvas::{Event, Interaction, Press};
25use crate::gesture;
26use crate::path::BlockPath;
27use crate::theme::{Style, Theme};
28use crate::tools::commands;
29use crate::tools::tool::{Action, Tool, ToolTrait};
30use blockworx_doc::repo::Repo;
31
32use super::lowering::{SIM_DT, SimFrame};
33
34pub struct SimTarget<'a> {
39 pub repo: &'a mut Repo,
40 pub index: &'a mut DocIndex,
41 pub path: &'a BlockPath,
42 pub presentation: &'a mut crate::presentation::Presentation,
43 pub gesture: &'a mut Gesture,
46 pub tool: &'a mut Tool,
47}
48
49#[derive(Default)]
50pub struct SimDriver {
51 pending_enter: bool,
54 pointer: Option<Pos2>,
58 press: Option<Press>,
61 dragging: bool,
64}
65
66impl SimDriver {
67 pub fn apply(
72 &mut self,
73 frame: SimFrame,
74 target: SimTarget<'_>,
75 theme: &Theme,
76 egui_painter: &egui::Painter,
77 ) {
78 let SimTarget {
79 repo,
80 index,
81 path,
82 presentation,
83 gesture,
84 tool,
85 } = target;
86 if let Some(name) = frame.switch_tool {
87 *tool = Tool::from_name(name);
90 }
91 self.track(frame.event);
92 self.press = frame.press;
93 let interaction = self.interaction(&frame);
94 let mut painter = Painter::headless(egui_painter.clone(), theme.palette().clone());
95 painter.set_scripted(crate::canvas::painter::ScriptedInput {
100 pointer: self.pointer,
101 });
102 *gesture = gesture::Gesture::open(
108 crate::edit::describe::Label::verb(tool.name().verb()),
109 Writability::Writable,
110 );
111 let action = {
112 let mut drawing = gesture::drawing(repo, index, path, presentation, gesture);
113 let mut style = Style::new(theme, &mut painter);
114 crate::tools::tool::frame(tool, &mut drawing, &interaction, &mut style)
115 };
116 gesture::close(repo, index, path, presentation, gesture);
117 if let Some(typing) = frame.typing
121 && let Some(edit) = painter.take_edit_text()
122 {
123 let mut buffer = edit.buffer.borrow_mut();
124 buffer.clear();
125 buffer.push_str(typing.text);
126 if typing.commit {
127 self.pending_enter = true;
128 }
129 }
130 if let Some(action) = action
134 && let Some(other) = dispatch(
135 action,
136 SimTarget {
137 repo,
138 index,
139 path,
140 presentation,
141 gesture,
142 tool,
143 },
144 )
145 {
146 tracing::warn!(
147 "scripted frame ignoring app-level action: {}",
148 action_name(&other)
149 );
150 }
151 if let Some(name) = frame.command {
152 run_command(
153 name,
154 SimTarget {
155 repo,
156 index,
157 path,
158 presentation,
159 gesture,
160 tool,
161 },
162 );
163 }
164 }
165
166 pub fn idle_frame(&self) -> SimFrame {
171 SimFrame {
172 event: (!self.dragging)
173 .then_some(self.pointer)
174 .flatten()
175 .map(Event::HoverAt),
176 press: self.press,
177 ..SimFrame::default()
178 }
179 }
180
181 pub fn idle_interaction(&mut self) -> Interaction {
185 let idle = self.idle_frame();
186 self.interaction(&idle)
187 }
188
189 pub fn pointer(&self) -> Option<Pos2> {
190 self.pointer
191 }
192
193 pub fn enter_pending(&self) -> bool {
196 self.pending_enter
197 }
198
199 #[cfg(test)]
200 pub fn queue_enter(&mut self) {
201 self.pending_enter = true;
202 }
203
204 fn interaction(&mut self, frame: &SimFrame) -> Interaction {
205 Interaction {
206 event: frame.event,
207 press: frame.press,
208 lost_focus: false,
209 enter_pressed: std::mem::take(&mut self.pending_enter),
210 tab_pressed: false,
211 escape_pressed: false,
212 delete_pressed: false,
213 shift: false,
214 }
215 }
216
217 fn track(&mut self, event: Option<Event>) {
219 match event {
220 Some(
221 Event::HoverAt(pos)
222 | Event::Clicked { pos }
223 | Event::DoubleClicked { pos }
224 | Event::Dragging { pos, .. },
225 ) => self.pointer = Some(pos),
226 Some(Event::DragStarted { pos }) => {
227 self.pointer = Some(pos);
228 self.dragging = true;
229 }
230 Some(Event::DragStopped { pos }) => {
231 self.pointer = Some(pos);
232 self.dragging = false;
233 }
234 None => {}
235 }
236 }
237}
238
239fn dispatch(action: Action, target: SimTarget<'_>) -> Option<Box<Action>> {
243 let SimTarget {
244 repo,
245 index,
246 path,
247 presentation,
248 gesture,
249 tool,
250 } = target;
251 *gesture = gesture::Gesture::open(
252 crate::edit::describe::Label::verb(action.label()),
253 Writability::Writable,
254 );
255 let outcome = {
256 let mut drawing = gesture::drawing(repo, index, path, presentation, gesture);
257 commands::apply_scripted(action, &mut drawing)
258 };
259 gesture::close(repo, index, path, presentation, gesture);
260 match outcome {
261 commands::ScriptedApply::Applied(settles_on) => {
262 if let Some(next) = settles_on {
263 *tool = next;
264 }
265 None
266 }
267 commands::ScriptedApply::NeedsApp(other) => Some(other),
268 }
269}
270
271fn run_command(name: &str, target: SimTarget<'_>) {
277 let SimTarget {
278 repo,
279 index,
280 path,
281 presentation,
282 gesture,
283 tool,
284 } = target;
285 let action = {
286 let history = commands::History {
287 can_undo: repo.can_undo(),
288 can_redo: repo.can_redo(),
289 };
290 let drawing = gesture::drawing(repo, index, path, presentation, gesture);
291 let current_lock: crate::edit::naming::InterfaceLock = drawing.current_locked().into();
292 let mut set = commands::CommandSet::available(&commands::CommandContext {
293 tool,
294 data: &drawing,
295 history,
296 current_lock,
297 writability: crate::doc::Writability::Writable,
301 head: blockworx_doc::rev::Rev::ZERO,
302 saving: crate::doc::Saving::Withheld,
303 viewing: crate::doc::Viewing::Head,
304 });
305 set.take_by_name(name)
306 };
307 let Some(action) = action else {
308 tracing::error!("command {name:?} is not available here");
309 return;
310 };
311 if let Some(other) = dispatch(
312 action,
313 SimTarget {
314 repo,
315 index,
316 path,
317 presentation,
318 gesture,
319 tool,
320 },
321 ) {
322 tracing::error!(
323 "command {name:?} needs the app and cannot run in a script: {}",
324 action_name(&other)
325 );
326 }
327}
328
329const MAX_FRAMES_PER_SHOW: u32 = 8;
332
333const HITCH_CAP: Duration = Duration::from_millis(100);
336
337#[derive(Clone, Copy, PartialEq, Debug)]
339pub struct Speed(f32);
340
341impl Speed {
342 pub const HALF: Self = Self(0.5);
343 pub const NORMAL: Self = Self(1.0);
344 pub const DOUBLE: Self = Self(2.0);
345}
346
347impl std::fmt::Display for Speed {
348 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
349 write!(f, "{}", self.0)
350 }
351}
352
353#[derive(Default)]
356pub struct SimClock {
357 frames: u32,
359 carry: Duration,
361}
362
363impl SimClock {
364 pub fn begin_show(&mut self, dt: Duration, speed: Speed) -> u32 {
367 self.carry += dt.min(HITCH_CAP).mul_f32(speed.0);
368 let due = self.carry.as_nanos() / SIM_DT.as_nanos();
371 let n = due.min(u128::from(MAX_FRAMES_PER_SHOW)) as u32;
372 self.carry -= SIM_DT * n;
373 n
374 }
375
376 pub fn tick(&mut self) {
378 self.frames += 1;
379 }
380
381 pub fn elapsed(&self) -> Duration {
383 SIM_DT * self.frames
384 }
385}
386
387pub(crate) fn action_name(action: &Action) -> &'static str {
388 match action {
389 Action::SwitchTool(_) => "SwitchTool",
390 Action::Delete(_) => "Delete",
391 Action::Copy(_) => "Copy",
392 Action::CopyPins(_) => "CopyPins",
393 Action::Cut(_) => "Cut",
394 Action::CutPins(_) => "CutPins",
395 Action::SetPinTags { .. } => "SetPinTags",
396 Action::SetShapeTagHidden { .. } => "SetShapeTagHidden",
397 Action::FlipShapePins(_) => "FlipShapePins",
398 Action::FlipBlockVertical(_) => "FlipBlockVertical",
399 Action::SetBlockLocked { .. } => "SetBlockLocked",
400 Action::Paste(_) => "Paste",
401 Action::ExpandBlock(_) => "ExpandBlock",
402 Action::GoToPath(_) => "GoToPath",
403 Action::Zoom(_) => "Zoom",
404 Action::Camera(_) => "Camera",
405 Action::SetRole { .. } => "SetRole",
406 Action::SetPinsKind { .. } => "SetPinsKind",
407 Action::OpenRolePicker { .. } => "OpenRolePicker",
408 Action::OpenPinTypePicker { .. } => "OpenPinTypePicker",
409 Action::GoUp => "GoUp",
410 Action::NavSelect { .. } => "NavSelect",
411 Action::PathBack => "PathBack",
412 Action::PathForward => "PathForward",
413 Action::Undo => "Undo",
414 Action::Redo => "Redo",
415 Action::Nudge { .. } => "Nudge",
416 Action::ResetView => "ResetView",
417 Action::Export { .. } => "Export",
418 Action::ExportRev { .. } => "ExportRev",
419 Action::Import => "Import",
420 Action::Reroute(_) => "Reroute",
421 Action::RerouteBlock(_) => "RerouteBlock",
422 Action::ViewRev(_) => "ViewRev",
423 Action::ViewHead => "ViewHead",
424 Action::RestoreRev(_) => "RestoreRev",
425 Action::TagRev { .. } => "TagRev",
426 Action::OpenTutorial => "OpenTutorial",
427 Action::TutorialLoadLevel(_) => "TutorialLoadLevel",
428 Action::TutorialExit => "TutorialExit",
429 Action::SaveProjection => "SaveProjection",
430 #[cfg(not(target_arch = "wasm32"))]
432 Action::NewDocument => "NewDocument",
433 #[cfg(not(target_arch = "wasm32"))]
434 Action::PickFile(_) => "PickFile",
435 #[cfg(not(target_arch = "wasm32"))]
436 Action::OpenRecent(_) => "OpenRecent",
437 #[cfg(not(target_arch = "wasm32"))]
438 Action::RenameDocument(_) => "RenameDocument",
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn begin_show_caps_frames_and_conserves_carry() {
448 let mut clock = SimClock::default();
449 assert_eq!(clock.begin_show(Duration::from_secs(1), Speed::NORMAL), 6);
451 assert!(clock.carry < SIM_DT);
452 clock.carry = Duration::from_millis(200);
454 assert_eq!(
455 clock.begin_show(Duration::ZERO, Speed::NORMAL),
456 MAX_FRAMES_PER_SHOW
457 );
458 let banked = Duration::from_millis(200)
459 .checked_sub(SIM_DT * MAX_FRAMES_PER_SHOW)
460 .unwrap();
461 assert_eq!(clock.carry, banked);
462 clock.carry = Duration::ZERO;
464 assert_eq!(clock.begin_show(SIM_DT / 2, Speed::NORMAL), 0);
465 assert_eq!(clock.carry, SIM_DT / 2);
466 assert_eq!(clock.begin_show(SIM_DT / 2, Speed::NORMAL), 1);
468 assert!(clock.carry < SIM_DT);
469 }
470
471 #[test]
474 fn speed_scales_the_banked_time() {
475 let show = Duration::from_millis(50);
476 let due = |speed| SimClock::default().begin_show(show, speed);
477 assert_eq!(due(Speed::NORMAL), 3);
478 assert_eq!(due(Speed::HALF), 1);
479 assert_eq!(due(Speed::DOUBLE), 6);
480 }
481}