blockworx/tools/
new_image.rs1use std::time::Duration;
2
3use std::sync::mpsc::{Receiver, TryRecvError};
4
5use blockworx_doc::block_model::Asset;
6use blockworx_geom::{Pos2, Rect, WorldPx};
7
8use crate::theme::Style;
9use crate::{
10 grid::GRID_SIZE,
11 theme::Role,
12 tools::{
13 names::ToolName,
14 resize_block::ResizeBlock,
15 tool::{Action, ToolTrait},
16 },
17 widget::drawing::Drawing,
18};
19use blockworx_paint::{Canvas, Cursor, Event, Interaction, Waker};
20
21pub enum NewImage {
34 Idle,
35 Dragging {
36 start: Pos2,
37 },
38 Pending {
42 rx: Receiver<Option<Asset>>,
43 placement: Placement,
44 },
45}
46
47pub(crate) enum Placement {
50 Box(Rect),
51 Centered(Pos2),
52}
53
54impl ToolTrait for NewImage {
55 fn name(&self) -> ToolName {
56 ToolName::NewImage
57 }
58 fn widget<C: Canvas>(
59 &mut self,
60 data: &mut Drawing,
61 interaction: &Interaction,
62 painter: &mut Style<'_, C>,
63 ) -> Option<Action> {
64 crate::widget::display::widget(data, interaction, painter);
65 painter.set_cursor(Cursor::Crosshair);
66 match self {
67 NewImage::Idle => {
68 if let Some(Event::DragStarted { pos }) = interaction.event {
69 *self = NewImage::Dragging { start: pos };
70 }
71 }
72 NewImage::Dragging { start } => {
73 if let Some(Event::Dragging { pos, .. }) = interaction.event {
74 let rect = Rect::from_two_pos(*start, pos);
75 painter.rect(
76 rect,
77 WorldPx::ZERO,
78 Role::Transparent,
79 (1.0, Role::ImageDragBox),
80 );
81 } else if let Some(Event::DragStopped { pos }) = interaction.event {
82 let candidate = Rect::from_two_pos(*start, pos);
83 if candidate.width() > GRID_SIZE && candidate.height() > GRID_SIZE {
84 *self = spawn_dialog(painter, Placement::Box(candidate));
85 } else {
86 *self = NewImage::Idle;
87 }
88 }
89 }
90 NewImage::Pending { rx, placement } => match rx.try_recv() {
91 Ok(picked) => {
92 let action = picked
93 .map(|image| import_image(data, painter, placement, &image))
94 .unwrap_or_default();
95 *self = NewImage::Idle;
96 return Some(action);
97 }
98 Err(TryRecvError::Empty) => {
99 painter.request_repaint_after(Duration::from_millis(100));
100 }
101 Err(TryRecvError::Disconnected) => *self = NewImage::Idle,
102 },
103 }
104 None
105 }
106}
107
108fn spawn_dialog<C: Canvas>(painter: &Style<'_, C>, placement: Placement) -> NewImage {
110 NewImage::Pending {
111 rx: spawn_image_dialog(painter.waker()),
112 placement,
113 }
114}
115
116#[cfg(not(target_arch = "wasm32"))]
122pub(crate) fn spawn_image_dialog(waker: Waker) -> Receiver<Option<Asset>> {
123 let (tx, rx) = std::sync::mpsc::channel();
124 let opening = waker.clone();
125 std::thread::spawn(move || {
126 let image = rfd::FileDialog::new()
127 .add_filter("Image", &["svg", "png"])
128 .pick_file()
129 .and_then(|path| read_image(&path));
130 let _ = tx.send(image);
131 waker.wake();
132 });
133 opening.wake();
134 rx
135}
136
137#[cfg(target_arch = "wasm32")]
141pub(crate) fn spawn_image_dialog(waker: Waker) -> Receiver<Option<Asset>> {
142 let (tx, rx) = std::sync::mpsc::channel();
143 let opening = waker.clone();
144 wasm_bindgen_futures::spawn_local(async move {
145 let image = if let Some(file) = rfd::AsyncFileDialog::new()
146 .add_filter("Image", &["svg", "png"])
147 .pick_file()
148 .await
149 {
150 let is_png = file.file_name().to_ascii_lowercase().ends_with(".png");
151 let bytes = file.read().await;
152 if is_png {
153 Some(Asset::Png(bytes.into()))
154 } else {
155 String::from_utf8(bytes)
156 .ok()
157 .map(|text| Asset::Svg(text.into_bytes().into()))
158 }
159 } else {
160 None
161 };
162 let _ = tx.send(image);
163 waker.wake();
164 });
165 opening.wake();
166 rx
167}
168
169fn import_image<C: Canvas>(
174 data: &mut Drawing,
175 painter: &mut Style<'_, C>,
176 placement: &Placement,
177 image: &Asset,
178) -> Action {
179 if let Some(intrinsic) = painter.image_intrinsic_size(image) {
180 if !crate::edit::lower::asset_within_limit(image, "the picked image") {
181 return Action::default();
182 }
183 let placed = match placement {
184 Placement::Box(rect) => crate::edit::assets::Placement::Box(*rect),
185 Placement::Centered(center) => crate::edit::assets::Placement::Centered {
186 center: *center,
187 intrinsic,
188 },
189 };
190 let shape = data.add_image(placed, image);
191 return Action::SwitchTool(ResizeBlock::Selected { shape }.into());
192 }
193 tracing::error!("Failed to import image: not a valid image");
194 Action::default()
195}
196
197#[cfg(not(target_arch = "wasm32"))]
201fn read_image(path: &std::path::Path) -> Option<Asset> {
202 let is_png = path
203 .extension()
204 .is_some_and(|ext| ext.eq_ignore_ascii_case("png"));
205 if is_png {
206 std::fs::read(path)
207 .ok()
208 .map(|bytes| Asset::Png(bytes.into()))
209 } else {
210 std::fs::read_to_string(path)
211 .ok()
212 .map(|text| Asset::Svg(text.into_bytes().into()))
213 }
214}