blockworx/exchange.rs
1//! What comes back into the document from outside it: the import, and the
2//! image pick. Each is the shell's own effect — this owns every substate of
3//! the dialog, what a cancel means included — and its end lands in the session
4//! as an ordinary command, so nothing below the shell waits on a dialog. Which
5//! picker opens is [`Ask`]'s, so a test answers one without a window.
6//!
7//! An export goes the other way and is not here: it is a command the editor
8//! executes, and the bytes come back through the session's hand-off slot.
9
10use crate::dialogs::Ask;
11use crate::kernel::Session;
12use crate::tools::tool::Action;
13
14/// What an import file dialog delivers: the picked file's name and bytes, or
15/// `None` if the user cancelled.
16type ImportReceiver = std::sync::mpsc::Receiver<Option<(String, Vec<u8>)>>;
17
18/// What an image dialog delivers: the picked artwork, or `None` if the user
19/// cancelled or the file could not be read.
20type ImageReceiver = std::sync::mpsc::Receiver<Option<blockworx_doc::block_model::Asset>>;
21
22/// What an open image pick is for, which is the shell's own record of the flow
23/// it is in: the artwork lands as a block's icon, or as an image of its own.
24#[derive(Clone, Copy)]
25pub(crate) enum Pick {
26 Icon(blockworx_doc::id::BlockId),
27 Image,
28}
29
30#[derive(Default)]
31pub(crate) struct Exchange {
32 /// The channel an in-flight import dialog delivers its picked file on.
33 /// Polled each frame; `None` when no import is pending.
34 pending_import: Option<ImportReceiver>,
35 /// The channel an in-flight image dialog delivers its pick on, and what
36 /// the pick is for. Polled each frame; `None` when no image is pending.
37 pending_image: Option<(Pick, ImageReceiver)>,
38}
39
40impl Exchange {
41 /// Open the import dialog (JSON / PNG / SVG).
42 pub(crate) fn import(&mut self, ask: &mut Ask<'_>) {
43 self.pending_import = Some(ask.import());
44 }
45
46 /// Open the image picker for whatever the artwork is wanted for.
47 pub(crate) fn pick(&mut self, ask: &mut Ask<'_>, what: Pick) {
48 self.pending_image = Some((what, ask.image()));
49 }
50
51 /// Deliver a picked import file once the off-thread dialog resolves.
52 pub(crate) fn poll_pending_import(&mut self, ctx: &egui::Context, session: &mut Session) {
53 let Some(rx) = &self.pending_import else {
54 return;
55 };
56 match rx.try_recv() {
57 Ok(Some((name, bytes))) => {
58 self.pending_import = None;
59 session.handle_imported(&name, bytes);
60 }
61 // A cancelled dialog and a dropped sender both end the import.
62 Ok(None) | Err(std::sync::mpsc::TryRecvError::Disconnected) => {
63 self.pending_import = None;
64 }
65 // Poll the open dialog at a gentle cadence, not per-frame —
66 // a native picker can sit open for minutes.
67 Err(std::sync::mpsc::TryRecvError::Empty) => {
68 ctx.request_repaint_after(std::time::Duration::from_millis(100));
69 }
70 }
71 }
72
73 /// Deliver a picked image once the off-thread dialog resolves — to the
74 /// same target the pick was opened for, so one that took minutes still
75 /// lands where the press that asked for it meant it to.
76 pub(crate) fn poll_pending_image(&mut self, ctx: &egui::Context) -> Option<Action> {
77 let (_, rx) = self.pending_image.as_ref()?;
78 match rx.try_recv() {
79 Ok(asset) => self
80 .pending_image
81 .take()
82 .and_then(|(what, _)| picked(what, asset)),
83 // A dropped sender ends the pick with nothing to place.
84 Err(std::sync::mpsc::TryRecvError::Disconnected) => {
85 self.pending_image = None;
86 None
87 }
88 // See `poll_pending_import`: a native picker can sit open for
89 // minutes, so poll it gently.
90 Err(std::sync::mpsc::TryRecvError::Empty) => {
91 ctx.request_repaint_after(std::time::Duration::from_millis(100));
92 None
93 }
94 }
95 }
96}
97
98/// What a finished image pick sends in: the artwork, as the command that
99/// writes it. A cancel — which is the shell's to read — sends nothing at all,
100/// because opening the picker changed nothing to put back.
101fn picked(what: Pick, asset: Option<blockworx_doc::block_model::Asset>) -> Option<Action> {
102 let asset = asset?;
103 Some(match what {
104 Pick::Image => Action::PlaceImage { asset },
105 Pick::Icon(block) => Action::SetIcon { block, asset },
106 })
107}