1use egui::{Pos2, Vec2};
10
11use crate::canvas::Interaction;
12use crate::canvas::image::ImageRegistry;
13use crate::canvas::painter::Painter;
14use crate::document_ng::{BlockPath, Document};
15use crate::icons::Icons;
16use crate::schema::model as schema;
17use crate::theme::{Style, Theme};
18#[cfg(test)]
19use crate::tools::RenameTitle;
20use crate::tools::SelectTool;
21use crate::tools::tool::{Action, Tool, ToolTrait};
22use crate::widget::drawing::{Drawing, finalize_load};
23
24use super::lowering::SimFrame;
25
26pub struct Session {
27 document: Document,
28 path: BlockPath,
29 tool: Tool,
30 images: std::rc::Rc<std::cell::RefCell<ImageRegistry>>,
31 icons: Icons,
32 pending_enter: bool,
35 pointer: Option<Pos2>,
39 dragging: bool,
42}
43
44impl Session {
45 pub fn new(initial_kdl: Option<&str>, name: &str) -> Self {
50 let document =
51 match initial_kdl.map(|src| crate::document_ng::schema_convert::from_kdl(src, name)) {
52 Some(Ok(mut doc)) => {
53 finalize_load(&mut doc);
54 doc
55 }
56 Some(Err(e)) => {
57 tracing::error!("session initial for {name} failed to parse: {e:?}");
58 Document::default()
59 }
60 None => Document::default(),
61 };
62 Self {
63 document,
64 path: BlockPath::empty(),
65 tool: Tool::Select(SelectTool),
66 images: std::rc::Rc::new(std::cell::RefCell::new(ImageRegistry::default())),
67 icons: Icons::default(),
68 pending_enter: false,
69 pointer: None,
70 dragging: false,
71 }
72 }
73
74 pub fn idle_frame(&self) -> SimFrame {
79 let event = (!self.dragging)
80 .then_some(self.pointer)
81 .flatten()
82 .map(crate::canvas::Event::HoverAt);
83 SimFrame {
84 event,
85 ..SimFrame::default()
86 }
87 }
88
89 fn track_pointer(&mut self, event: Option<crate::canvas::Event>) {
91 use crate::canvas::Event;
92 match event {
93 Some(
94 Event::HoverAt(pos)
95 | Event::Clicked { pos }
96 | Event::DoubleClicked { pos }
97 | Event::Dragging { pos, .. },
98 ) => self.pointer = Some(pos),
99 Some(Event::DragStarted { pos }) => {
100 self.pointer = Some(pos);
101 self.dragging = true;
102 }
103 Some(Event::DragStopped { pos }) => {
104 self.pointer = Some(pos);
105 self.dragging = false;
106 }
107 None => {}
108 }
109 }
110
111 pub fn step(&mut self, frame: SimFrame, theme: &Theme, egui_painter: &egui::Painter) {
116 if let Some(name) = frame.switch_tool {
117 self.tool = Tool::from_name(name);
118 }
119 self.track_pointer(frame.event);
120 let interaction = Interaction {
121 event: frame.event,
122 lost_focus: false,
123 enter_pressed: std::mem::take(&mut self.pending_enter),
124 tab_pressed: false,
125 escape_pressed: false,
126 delete_pressed: false,
127 shift: false,
128 };
129 let mut painter = Painter::new(
130 egui_painter.clone(),
131 Pos2::ZERO,
132 1.0,
133 Vec2::ZERO,
134 theme.palette().clone(),
135 self.images.clone(),
136 self.icons.clone(),
137 );
138 painter.set_scripted(crate::canvas::painter::ScriptedInput {
143 pointer: self.pointer,
144 });
145 let mut drawing = Drawing::new(&mut self.document, &self.path);
146 let action = {
147 let mut style = Style::new(theme, &mut painter);
148 self.tool.widget(&mut drawing, &interaction, &mut style)
149 };
150 if let Some(typing) = frame.typing
154 && let Some(edit) = painter.take_edit_text()
155 {
156 let mut buffer = edit.buffer.borrow_mut();
157 buffer.clear();
158 buffer.push_str(typing.text);
159 if typing.commit {
160 self.pending_enter = true;
161 }
162 }
163 if let Some(action) = action {
164 self.apply(action);
165 }
166 }
167
168 #[cfg(test)]
172 pub fn needs_commit_frame(&self) -> bool {
173 self.pending_enter
174 }
175
176 fn apply(&mut self, action: Action) {
180 match action {
181 Action::SwitchTool(tool) => self.tool = tool,
182 other => {
183 tracing::warn!(
184 "tutorial session ignoring non-tool action: {}",
185 action_name(&other)
186 );
187 }
188 }
189 }
190
191 pub fn projected(&self) -> schema::Document {
192 schema::Document::from(&self.document)
193 }
194
195 pub fn tool_name(&self) -> crate::tools::names::ToolName {
197 self.tool.name()
198 }
199
200 #[cfg(test)]
203 pub fn renaming(&self) -> bool {
204 matches!(self.tool, Tool::RenameTitle(RenameTitle::Renaming { .. }))
205 }
206}
207
208fn action_name(action: &Action) -> &'static str {
209 match action {
210 Action::SwitchTool(_) => "SwitchTool",
211 Action::Delete(_) => "Delete",
212 Action::Copy(_) => "Copy",
213 Action::CopyPins(_) => "CopyPins",
214 Action::Cut(_) => "Cut",
215 Action::CutPins(_) => "CutPins",
216 Action::SetPinTags { .. } => "SetPinTags",
217 Action::SetShapeTagHidden { .. } => "SetShapeTagHidden",
218 Action::FlipShapePins(_) => "FlipShapePins",
219 Action::FlipBlockVertical(_) => "FlipBlockVertical",
220 Action::SetBlockLocked { .. } => "SetBlockLocked",
221 Action::Paste(_) => "Paste",
222 Action::ExpandBlock(_) => "ExpandBlock",
223 Action::OpenRolePicker { .. } => "OpenRolePicker",
224 Action::OpenPinTypePicker { .. } => "OpenPinTypePicker",
225 Action::GoUp => "GoUp",
226 Action::NavSelect { .. } => "NavSelect",
227 Action::PathBack => "PathBack",
228 Action::PathForward => "PathForward",
229 Action::Undo => "Undo",
230 Action::Redo => "Redo",
231 Action::Nudge { .. } => "Nudge",
232 Action::ResetView => "ResetView",
233 Action::Export { .. } => "Export",
234 Action::Import => "Import",
235 Action::Reroute(_) => "Reroute",
236 Action::RerouteBlock(_) => "RerouteBlock",
237 Action::OpenTutorial => "OpenTutorial",
238 Action::TutorialLoadLevel(_) => "TutorialLoadLevel",
239 Action::TutorialExit => "TutorialExit",
240 }
241}