//! Tool hosting: the active tool receives pointer/key events and its actions are executed here. use std::sync::Arc; use designcraft_geom::Unit; pub use designcraft_tools::SnapView; use designcraft_tools::{Action, CanvasLayout, Cursor, Mods, Overlay, PointerEvent, ToolContext, ToolKey}; use serde::Serialize; use serde_json::Value; use crate::{EngineError, HistoryEntry, Interaction, Result, Session, record_undo}; /// Something the UI should do in response to a tool or command. #[derive(Clone, Debug, PartialEq, Serialize)] #[serde(tag = "kind", rename_all = "camelCase")] pub enum UiRequest { Dialog { id: String, params: Value, }, View { params: Value, }, /// Open a file picker for the given purpose (`place`, `open`, `saveAs`, `export`). Pick { purpose: String, params: Value, }, } /// View information the UI passes with pointer events. #[derive(Clone, Copy, Debug, PartialEq, Serialize)] pub struct ViewInfo { /// Screen pixels per point. pub zoom: f64, pub snap: SnapView, pub unit: Unit, } impl ViewInfo { pub const fn at_zoom(zoom: f64) -> Self { Self { zoom, snap: SnapView::FACTORY, unit: Unit::Points } } } impl Default for ViewInfo { fn default() -> Self { Self::at_zoom(1.0) } } impl Session { pub fn tool_id(&self) -> &'static str { self.tool.id() } pub fn set_tool(&mut self, id: &str) { if self.tool.id() == id { return; } self.tool = designcraft_tools::create(id); // Leaving the Type tool keeps the text frame selected. if id != "type" && let Some(st) = self.active_mut() && let Some(t) = st.selection.text.take() && let Some(f) = t.frame.or_else(|| st.doc.story(t.story).and_then(|s| s.frames.first().copied())) { st.selection.items = vec![f]; st.selection.cells = None; } } pub fn layout(&self) -> CanvasLayout { match self.active() { Some(st) => CanvasLayout::new(&st.doc, st.editing_parents), None => CanvasLayout::default(), } } fn with_ctx(&mut self, view: ViewInfo, f: impl FnOnce(&mut dyn designcraft_tools::Tool, &ToolContext) -> T) -> Option { let st = self.active()?; let doc = st.doc.clone(); let sel = st.selection.clone(); let layer = st.active_layer; let layout = CanvasLayout::new(&doc, st.editing_parents); let cache = self.cache.clone(); let cx = ToolContext { doc: &doc, selection: &sel, cache: &cache, layout: &layout, zoom: view.zoom.max(1e-6), layer, snap: view.snap, unit: view.unit, }; Some(f(self.tool.as_mut(), &cx)) } pub fn pointer(&mut self, ev: &PointerEvent, view: ViewInfo) -> Result<()> { let actions = self.with_ctx(view, |t, cx| t.pointer(cx, ev)).unwrap_or_default(); self.run_actions(actions) } pub fn tool_key(&mut self, key: ToolKey, mods: Mods, view: ViewInfo) -> Result { let actions = self.with_ctx(view, |t, cx| t.key(cx, key, mods)).unwrap_or_default(); let handled = !actions.is_empty(); self.run_actions(actions)?; Ok(handled) } pub fn overlays(&mut self, view: ViewInfo) -> Vec { self.with_ctx(view, |t, cx| t.overlays(cx)).unwrap_or_default() } pub fn cursor(&mut self, p: designcraft_geom::Point, mods: Mods, view: ViewInfo) -> Cursor { self.with_ctx(view, |t, cx| t.cursor(cx, p, mods)).unwrap_or_default() } pub fn tool_busy(&self) -> bool { self.tool.busy() } pub fn wants_text(&mut self) -> bool { self.with_ctx(ViewInfo::at_zoom(1.0), |t, cx| t.wants_text(cx)).unwrap_or(false) } pub fn run_actions(&mut self, actions: Vec) -> Result<()> { for a in actions { match a { Action::Begin(label) => self.begin_interaction(&label)?, Action::Preview(cmd, p) => self.preview(&cmd, p)?, Action::Commit => self.commit_interaction()?, Action::Cancel => self.cancel_interaction(), Action::Exec(cmd, p) => { self.execute(&cmd, &p)?; } Action::Dialog(id, params) => self.ui_requests.push(UiRequest::Dialog { id, params }), Action::SwitchTool(id) => self.set_tool(&id), Action::View(params) => self.ui_requests.push(UiRequest::View { params }), } } Ok(()) } pub fn begin_interaction(&mut self, label: &str) -> Result<()> { let st = self.doc_mut()?; st.interaction = Some(Interaction { label: label.into(), doc: st.doc.clone(), selection: st.selection.clone(), preview: None }); Ok(()) } /// Apply `cmd` on top of the interaction's snapshot (replacing the previous preview). pub fn preview(&mut self, cmd: &str, params: Value) -> Result<()> { let st = self.doc_mut()?; let Some(it) = st.interaction.clone() else { return Err(EngineError::Other("no interaction".into())) }; st.doc = it.doc.clone(); st.selection = it.selection.clone(); let r = self.execute(cmd, ¶ms); if let Some(st) = self.active_mut() && let Some(i) = st.interaction.as_mut() { i.preview = Some((cmd.to_string(), params)); } // Journal only the committed result. if r.is_ok() { self.journal.pop(); } r.map(|_| ()) } pub fn commit_interaction(&mut self) -> Result<()> { let st = self.doc_mut()?; let Some(it) = st.interaction.take() else { return Ok(()) }; if !Arc::ptr_eq(&it.doc, &st.doc) { record_undo(st, HistoryEntry { label: it.label, doc: it.doc, selection: it.selection }); } if let Some((c, p)) = it.preview { self.journal.push((c, p)); } // The interaction's last preview is the transform to repeat; the next one starts afresh. self.transforms.2 = false; Ok(()) } pub fn cancel_interaction(&mut self) { if let Some(st) = self.active_mut() && let Some(it) = st.interaction.take() { st.doc = it.doc; st.selection = it.selection; st.revision += 1; if self.transforms.2 { self.transforms.0.pop(); self.transforms.2 = false; } } } }