mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-07 11:02:52 +00:00
File › Place (native picker, browser import, Place with Import Options and the PDF page dialog) passes loadCursor to file.place. With no empty or graphic frame selected and no x/y given, an image is added to the document and loaded into the place cursor instead of landing in a new frame on the first spread; a click places it at its natural size, a drag draws its frame. The loaded graphic keeps the chosen PDF page and crop box. Escape or choosing another tool unloads the cursor. Dropped files and file.place without loadCursor behave as before.
203 lines
6.6 KiB
Rust
203 lines
6.6 KiB
Rust
//! 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);
|
|
// Any other tool unloads the place cursor.
|
|
if id != "placeGun" {
|
|
self.loaded = None;
|
|
}
|
|
// 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<T>(&mut self, view: ViewInfo, f: impl FnOnce(&mut dyn designcraft_tools::Tool, &ToolContext) -> T) -> Option<T> {
|
|
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<bool> {
|
|
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<Overlay> {
|
|
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<Action>) -> 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;
|
|
}
|
|
}
|
|
}
|
|
}
|