1use egui::{Pos2, Vec2};
11
12use crate::canvas::Interaction;
13use crate::canvas::image::ImageRegistry;
14use crate::canvas::painter::Painter;
15use crate::document_ng::{BlockPath, Document};
16use crate::icons::Icons;
17use crate::schema::model as schema;
18use crate::theme::{Style, Theme};
19use crate::tools::tool::{Action, Tool, ToolTrait};
20use crate::tools::{RenameTitle, SelectTool};
21use crate::widget::drawing::{Drawing, finalize_load};
22
23use super::lowering::{Lowering, SimFrame, UnresolvedTarget};
24use super::script::Script;
25
26pub struct Runner {
29 ctx: egui::Context,
30 document: Document,
31 path: BlockPath,
32 tool: Tool,
33 theme: Theme,
34 images: std::rc::Rc<std::cell::RefCell<ImageRegistry>>,
35 icons: Icons,
36 pending_enter: bool,
39}
40
41impl Runner {
42 pub fn new(initial_kdl: Option<&str>, name: &str) -> Self {
47 let document =
48 match initial_kdl.map(|src| crate::document_ng::schema_convert::from_kdl(src, name)) {
49 Some(Ok(mut doc)) => {
50 finalize_load(&mut doc);
51 doc
52 }
53 Some(Err(e)) => {
54 tracing::error!("runner initial for {name} failed to parse: {e:?}");
55 Document::default()
56 }
57 None => Document::default(),
58 };
59 let ctx = egui::Context::default();
60 ctx.set_fonts(crate::font::build_fonts(
64 crate::preferences::FontChoice::default(),
65 ));
66 let _ = ctx.run_ui(egui::RawInput::default(), |_| {});
67 Self {
68 ctx,
69 document,
70 path: BlockPath::empty(),
71 tool: Tool::Select(SelectTool),
72 theme: Theme::default(),
73 images: std::rc::Rc::new(std::cell::RefCell::new(ImageRegistry::default())),
74 icons: Icons::default(),
75 pending_enter: false,
76 }
77 }
78
79 pub fn run_script(&mut self, script: &Script) -> Result<(), UnresolvedTarget> {
82 let mut lowering = Lowering::new(script);
83 loop {
84 let projected = self.projected();
85 match lowering.next(&projected)? {
86 Some(frame) => self.step(frame),
87 None => break,
88 }
89 }
90 if self.pending_enter {
93 self.step(SimFrame::default());
94 }
95 Ok(())
96 }
97
98 pub fn step(&mut self, frame: SimFrame) {
102 if let Some(name) = frame.switch_tool {
103 self.tool = Tool::from_name(name);
104 }
105 let interaction = Interaction {
106 event: frame.event,
107 lost_focus: false,
108 enter_pressed: std::mem::take(&mut self.pending_enter),
109 tab_pressed: false,
110 escape_pressed: false,
111 delete_pressed: false,
112 shift: false,
113 };
114 let Self {
115 ctx,
116 document,
117 path,
118 tool,
119 theme,
120 images,
121 icons,
122 pending_enter,
123 } = self;
124 let mut action = None;
125 let _ = ctx.clone().run_ui(egui::RawInput::default(), |ui| {
126 let egui_painter = ui.ctx().layer_painter(egui::LayerId::new(
127 egui::Order::Background,
128 egui::Id::new("tutorial_runner"),
129 ));
130 let mut painter = Painter::new(
131 egui_painter,
132 Pos2::ZERO,
133 1.0,
134 Vec2::ZERO,
135 theme.palette().clone(),
136 images.clone(),
137 icons.clone(),
138 );
139 let mut drawing = Drawing::new(document, path);
140 {
141 let mut style = Style::new(theme, &mut painter);
142 action = tool.widget(&mut drawing, &interaction, &mut style);
143 }
144 if let Some(typing) = frame.typing
148 && let Some(edit) = painter.take_edit_text()
149 {
150 let mut buffer = edit.buffer.borrow_mut();
151 buffer.clear();
152 buffer.push_str(typing.text);
153 if typing.commit {
154 *pending_enter = true;
155 }
156 }
157 });
158 if let Some(action) = action {
159 self.apply(action);
160 }
161 }
162
163 fn apply(&mut self, action: Action) {
167 match action {
168 Action::SwitchTool(tool) => self.tool = tool,
169 other => {
170 tracing::warn!(
171 "tutorial runner ignoring non-tool action: {}",
172 action_name(&other)
173 );
174 }
175 }
176 }
177
178 pub fn projected(&self) -> schema::Document {
179 schema::Document::from(&self.document)
180 }
181
182 pub fn document(&self) -> &Document {
183 &self.document
184 }
185
186 pub fn renaming(&self) -> bool {
189 matches!(self.tool, Tool::RenameTitle(RenameTitle::Renaming { .. }))
190 }
191}
192
193fn action_name(action: &Action) -> &'static str {
194 match action {
195 Action::SwitchTool(_) => "SwitchTool",
196 Action::Delete(_) => "Delete",
197 Action::Copy(_) => "Copy",
198 Action::CopyPins(_) => "CopyPins",
199 Action::Cut(_) => "Cut",
200 Action::CutPins(_) => "CutPins",
201 Action::SetPinTags { .. } => "SetPinTags",
202 Action::SetShapeTagHidden { .. } => "SetShapeTagHidden",
203 Action::FlipShapePins(_) => "FlipShapePins",
204 Action::FlipBlockVertical(_) => "FlipBlockVertical",
205 Action::SetBlockLocked { .. } => "SetBlockLocked",
206 Action::Paste(_) => "Paste",
207 Action::ExpandBlock(_) => "ExpandBlock",
208 Action::OpenRolePicker { .. } => "OpenRolePicker",
209 Action::OpenPinTypePicker { .. } => "OpenPinTypePicker",
210 Action::GoUp => "GoUp",
211 Action::NavSelect { .. } => "NavSelect",
212 Action::PathBack => "PathBack",
213 Action::PathForward => "PathForward",
214 Action::Undo => "Undo",
215 Action::Redo => "Redo",
216 Action::Nudge { .. } => "Nudge",
217 Action::ResetView => "ResetView",
218 Action::Export { .. } => "Export",
219 Action::Import => "Import",
220 Action::Reroute(_) => "Reroute",
221 Action::RerouteBlock(_) => "RerouteBlock",
222 Action::OpenTutorial => "OpenTutorial",
223 Action::TutorialLoadLevel(_) => "TutorialLoadLevel",
224 Action::TutorialExit => "TutorialExit",
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use crate::tutorial::level::all_levels;
232
233 #[test]
239 fn every_level_replays_to_its_solution() {
240 for level in all_levels() {
241 let mut runner = Runner::new(level.initial_kdl, &level.id);
242 runner.run_script(&level.script).unwrap_or_else(|e| {
243 panic!("{}: demo references a missing object: {e:?}", level.id)
244 });
245 let replayed = runner.projected();
246 let mut solution =
247 crate::document_ng::schema_convert::from_kdl(level.solution_kdl, &level.id)
248 .unwrap_or_else(|e| panic!("{}: solution failed to parse: {e:?}", level.id));
249 finalize_load(&mut solution);
250 let solution = schema::Document::from(&solution);
251 assert_eq!(
252 replayed,
253 solution,
254 "{}: replay diverged from the stored solution.\n-- replayed --\n{}\n-- solution --\n{}",
255 level.id,
256 replayed.to_kdl(),
257 solution.to_kdl()
258 );
259 }
260 }
261
262 #[test]
266 fn block_placement_drops_into_the_title_editor() {
267 let levels = all_levels();
268 let first = &levels[0];
269 let mut runner = Runner::new(first.initial_kdl, &first.id);
270 runner.run_script(&first.steps[0].script).unwrap();
271 runner.run_script(&first.steps[1].script).unwrap();
272 assert!(
273 runner.renaming(),
274 "placing the block should open its title editor"
275 );
276 }
277}