Skip to main content

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}