Skip to main content

blockworx_kernel/
dispatch.rs

1//! Applying one [`Action`] to the session.
2//!
3//! The document-scoped arms live in `commands::apply_scripted`, shared with
4//! the script engine so a scripted session and a live one cannot drift. What
5//! this adds is the arms that also move the editor: the clipboard, the
6//! history walk, navigation, the lens, the camera, and the palette.
7//!
8//! [`Action`] is exactly what the core executes. A file dialog, a picker
9//! window, a document door belong to the shell, which performs them itself and
10//! never asks the core about one — so no action is handed back here. What an
11//! action *produces* — the bytes of an export, the text of a copy — is left in
12//! the session's [`Handoff`] list for the host to take.
13
14use blockworx_doc::{block_model::Asset, id::BlockId};
15use blockworx_editor::{
16    edit::{assets, describe::Label, lower::asset_within_limit},
17    shape::ShapeId,
18};
19use blockworx_paint::{TextLayout, ZoomStep};
20use blockworx_tools::{
21    SelectTool,
22    commands::{ScriptedApply, apply_transition},
23    resize_block::ResizeBlock,
24    tool::{Action, Tool, Transition},
25};
26
27use crate::{handoff::Handoff, session::Session};
28
29impl Session {
30    /// Apply one action, or a tool's hand-off — all of it, since that is what
31    /// the core executes and nothing else reaches here.
32    ///
33    /// `layout` is the host's text engine: an export lays its diagram out
34    /// through the engine the canvas paints with, so it breaks its lines where
35    /// the screen did.
36    pub fn dispatch(&mut self, transition: impl Into<Transition>, layout: &dyn TextLayout) {
37        let transition = transition.into();
38        let label = transition.label();
39        let outcome = self.commit_gesture(Label::verb(label), |drawing| {
40            apply_transition(transition, drawing)
41        });
42        let action = match outcome {
43            ScriptedApply::Applied(settles_on) => {
44                if let Some(tool) = settles_on {
45                    self.tool = tool;
46                }
47                return;
48            }
49            ScriptedApply::NeedsApp(action) => *action,
50            ScriptedApply::NotApplicable(effect) => {
51                unreachable!("the shell performs {} itself", effect.named())
52            }
53        };
54        match action {
55            Action::Copy(shapes) => {
56                let clip = self.drawing().copy_selection(&shapes);
57                self.copied(clip);
58            }
59            Action::CopyPins(pins) => {
60                let clip = self.drawing().copy_pins(&pins);
61                self.copied(clip);
62            }
63            Action::Cut(shapes) => {
64                let from = self.doc.session();
65                let clip =
66                    self.commit_gesture(Label::verb(label), |d| d.cut_selection(&shapes, from));
67                self.copied(clip);
68                self.tool = Tool::Select(SelectTool);
69            }
70            Action::CutPins(pins) => {
71                let from = self.doc.session();
72                let clip = self.commit_gesture(Label::verb(label), |d| d.cut_pins(&pins, from));
73                self.copied(clip);
74                self.tool = Tool::Select(SelectTool);
75            }
76            Action::Paste(text) => self.paste(&text),
77            Action::ExpandBlock(rid) => {
78                self.path.push(rid);
79                self.after_navigate();
80            }
81            Action::GoToPath(path) => {
82                self.path = path;
83                self.after_navigate();
84            }
85            Action::NavSelect { block, extend } => self.nav_select(block, extend.into()),
86            // Above the document root there is nowhere to go, and the registry
87            // withholds the command there so the button draws dead.
88            Action::FrameRect(rect) => self.fit_to_rect(rect),
89            Action::AcknowledgeFailure(failure) => {
90                self.failures
91                    .acknowledge(crate::chrome::Acknowledged(failure));
92            }
93            Action::GoUp => {
94                if self.path.pop().is_some() {
95                    self.after_navigate();
96                }
97            }
98            Action::Undo => self.step_history(blockworx_tools::history::Direction::Back),
99            Action::Redo => self.step_history(blockworx_tools::history::Direction::Forward),
100            Action::Nudge { dx, dy } => self.nudge_selection(dx, dy),
101            Action::ResetView => self.fit_view(),
102            Action::ToggleDiagnostic => self.diagnostic = self.diagnostic.toggled(),
103            Action::ToggleFrameRate => self.frame_rate = self.frame_rate.toggled(),
104            Action::Zoom(step) => self.zoom(step),
105            Action::ViewRev(rev) => self.view_rev(rev),
106            Action::ViewHead => self.view_head(),
107            Action::TagRev { at, name, how } => self.tag_rev(at, &name, how),
108            Action::SetPalette(palette) => self.paint_in(palette),
109            Action::SetIcon { block, asset } => self.set_icon(block, &asset, label),
110            Action::Export { format, selection } => {
111                let content = self.export_content(layout, format, selection);
112                let name = self.sheet.name.clone();
113                self.hands_back(Handoff::Export { content, name });
114            }
115            Action::PlaceImage { asset } => self.place_picked_image(&asset, label),
116            Action::StampTool { .. }
117            | Action::Arm(_)
118            | Action::ArmAddRouteLabel(_)
119            | Action::OpenEditor(_)
120            | Action::Delete(_)
121            | Action::SetPinTags { .. }
122            | Action::SetShapeTagHidden { .. }
123            | Action::FlipShapePins(_)
124            | Action::FlipBlockVertical(_)
125            | Action::SetBlockLocked { .. }
126            | Action::Reroute(_)
127            | Action::RerouteBlock(_)
128            | Action::SetRole { .. }
129            | Action::SetPinsKind { .. } => {
130                unreachable!("{label} is not the dispatcher's to apply")
131            }
132        }
133    }
134
135    /// Wear the artwork as the block's icon, left selected for repositioning.
136    /// Artwork the document will not carry writes nothing, and the block
137    /// itself is what stays selected.
138    fn set_icon(&mut self, block: BlockId, asset: &Asset, label: &'static str) {
139        let shape = if asset_within_limit(asset, "the picked icon") {
140            self.commit_gesture(Label::verb(label), |d| d.set_icon(block, asset));
141            ShapeId::Icon(block)
142        } else {
143            ShapeId::Rect(block)
144        };
145        self.tool = ResizeBlock::Selected { shape }.into();
146    }
147
148    /// Draw the artwork, left selected so the next gesture resizes it.
149    /// Artwork the document will not carry — over the limit, or not an image
150    /// at all — writes nothing and leaves the selection empty.
151    fn place_picked_image(&mut self, asset: &Asset, label: &'static str) {
152        let placed = asset_within_limit(asset, "the picked image")
153            .then(|| self.place_image(asset, label))
154            .flatten();
155        self.tool = match placed {
156            Some(shape) => ResizeBlock::Selected { shape }.into(),
157            None => Tool::Select(SelectTool),
158        };
159    }
160
161    /// The drawn image itself, or `None` for a file that is not one. The
162    /// picker knows nothing about the canvas, so where the artwork goes is
163    /// decided here: centred where a paste would land, at the size the file's
164    /// own aspect gives it.
165    fn place_image(&mut self, asset: &Asset, label: &'static str) -> Option<ShapeId> {
166        let intrinsic = blockworx_paint::image::image_intrinsic_size(asset)
167            .inspect_err(|_| tracing::error!("Failed to import image: not a valid image"))
168            .ok()?;
169        let placed = assets::Placement::Centered {
170            center: self.paste_target(),
171            intrinsic,
172        };
173        Some(self.commit_gesture(Label::verb(label), |d| d.add_image(placed, asset)))
174    }
175
176    /// One keyboard zoom step, about the pointer where there is one over the
177    /// canvas.
178    fn zoom(&mut self, step: ZoomStep) {
179        let pointer = self.pointer_screen();
180        self.zoom_step(step, pointer);
181    }
182
183    /// Leave whatever a copy or a cut yielded on the clipboard.
184    fn copied(&mut self, clip: Option<blockworx_editor::edit::clipboard::Clipboard>) {
185        if let Some(json) = clip.and_then(|clip| clip.to_json()) {
186            self.hands_back(Handoff::Clipboard(json));
187        }
188    }
189}