1#![deny(clippy::all)]
2#![forbid(unsafe_code)]
3use crate::autosave::{AUTOSAVE_INTERVAL_S, autosave};
4use crate::control::{Control, Info};
5use crate::drawme::ImageInfo;
6use crate::events::{Events, KeyCode};
7use crate::file_util::{DEFAULT_PRJ_PATH, get_prj_name};
8use crate::history::{History, Record};
9use crate::menu::{Menu, ToolSelectMenu, are_tools_active};
10use crate::result::trace_ok_err;
11use crate::tools::{
12 ALWAYS_ACTIVE_ZOOM, BBOX_NAME, Manipulate, ToolState, ToolWrapper, ZOOM_NAME, make_tool_vec,
13};
14use crate::util::Visibility;
15use crate::world::World;
16use crate::{Annotation, UpdateView, apply_tool_method_mut, httpserver, image_util, measure_time};
17use image::{DynamicImage, GenericImageView};
18use rvimage_domain::{BbI, PtI, RvResult, ShapeF};
19use std::fmt::Debug;
20use std::mem;
21use std::path::{Path, PathBuf};
22use std::sync::mpsc::Receiver;
23use std::time::Instant;
24use tracing::{error, info, warn};
25
26fn pos_2_string_gen<T>(im: &T, x: u32, y: u32) -> String
27where
28 T: GenericImageView,
29 <T as GenericImageView>::Pixel: Debug,
30{
31 let p = format!("{:?}", im.get_pixel(x, y));
32 format!("({x}, {y}) -> ({})", &p[6..p.len() - 2])
33}
34
35fn pos_2_string(im: &DynamicImage, x: u32, y: u32) -> String {
36 if x < im.width() && y < im.height() {
37 image_util::apply_to_matched_image(
38 im,
39 |im| pos_2_string_gen(im, x, y),
40 |im| pos_2_string_gen(im, x, y),
41 |im| pos_2_string_gen(im, x, y),
42 |im| pos_2_string_gen(im, x, y),
43 )
44 } else {
45 "".to_string()
46 }
47}
48
49fn get_pixel_on_orig_str(world: &World, mouse_pos: &Option<PtI>) -> Option<String> {
50 mouse_pos.map(|p| pos_2_string(world.data.im_background(), p.x, p.y))
51}
52
53fn apply_tools(
54 tools: &mut [ToolState],
55 mut world: World,
56 mut history: History,
57 input_event: &Events,
58) -> (World, History) {
59 let aaz = tools
60 .iter_mut()
61 .find(|t| t.name == ALWAYS_ACTIVE_ZOOM)
62 .unwrap();
63 (world, history) = apply_tool_method_mut!(aaz, events_tf, world, history, input_event);
64 let aaz_hbu = apply_tool_method_mut!(aaz, has_been_used, input_event);
65 let not_aaz = tools
66 .iter_mut()
67 .filter(|t| t.name != ALWAYS_ACTIVE_ZOOM && t.is_active());
68 for t in not_aaz {
69 (world, history) = apply_tool_method_mut!(t, events_tf, world, history, input_event);
70 if aaz_hbu == Some(true) {
71 (world, history) = apply_tool_method_mut!(t, on_always_active_zoom, world, history);
72 }
73 }
74 (world, history)
75}
76
77macro_rules! activate_tool_event {
78 ($key:ident, $name:expr, $input:expr, $rat:expr, $tools:expr) => {
79 if $input.held_alt() && $input.pressed(KeyCode::$key) {
80 $rat = Some(
81 $tools
82 .iter()
83 .enumerate()
84 .find(|(_, t)| t.name == $name)
85 .unwrap()
86 .0,
87 );
88 }
89 };
90}
91
92fn find_active_tool(tools: &[ToolState]) -> Option<&str> {
93 tools
94 .iter()
95 .find(|t| t.is_active() && !t.is_always_active())
96 .map(|t| t.name)
97}
98
99pub struct MainEventLoop {
100 menu: Menu,
101 tools_select_menu: ToolSelectMenu,
102 world: World,
103 ctrl: Control,
104 history: History,
105 tools: Vec<ToolState>,
106 recently_clicked_tool_idx: Option<usize>,
107 rx_from_http: Option<Receiver<RvResult<String>>>,
108 http_addr: String,
109 autosave_timer: Instant,
110 next_image_held_timer: Instant,
111}
112impl Default for MainEventLoop {
113 fn default() -> Self {
114 let file_path = std::env::args().nth(1).map(PathBuf::from);
115 Self::new(file_path)
116 }
117}
118
119impl MainEventLoop {
120 pub fn new(prj_file_path: Option<PathBuf>) -> Self {
121 let ctrl = Control::new();
122
123 let mut world = World::empty();
124 let mut tools = make_tool_vec();
125 for t in &mut tools {
126 if t.is_active() {
127 (world, _) = t.activate(world, History::default());
128 }
129 }
130 let http_addr = ctrl.http_address();
131 let rx_from_http = if let Ok((_, rx)) = httpserver::launch(http_addr.clone()) {
133 Some(rx)
134 } else {
135 None
136 };
137 let mut self_ = Self {
138 world,
139 ctrl,
140 tools,
141 http_addr,
142 tools_select_menu: ToolSelectMenu::default(),
143 menu: Menu::default(),
144 history: History::default(),
145 recently_clicked_tool_idx: None,
146 rx_from_http,
147 autosave_timer: Instant::now(),
148 next_image_held_timer: Instant::now(),
149 };
150
151 trace_ok_err(self_.load_prj_during_startup(prj_file_path));
152 self_
153 }
154 pub fn one_iteration(
155 &mut self,
156 e: &Events,
157 ui_image_rect: Option<ShapeF>,
158 tmp_anno_buffer: Option<Annotation>,
159 request_file_label_to_load: Option<&str>,
160 ui: &mut egui::Ui,
161 ) -> RvResult<(UpdateView, bool, bool, &str)> {
162 measure_time!("whole iteration", {
163 measure_time!("part 1", {
164 self.world.set_image_rect(ui_image_rect);
165 self.world.update_view.tmp_anno_buffer = tmp_anno_buffer;
166 let project_loaded_in_curr_iter = self.menu.ui(
167 ui,
168 &mut self.ctrl,
169 &mut self.world.data.tools_data_map,
170 find_active_tool(&self.tools),
171 );
172 let new_annos = self
173 .ctrl
174 .check_wand_many_output(&mut self.world.data.tools_data_map)?;
175 if new_annos {
176 self.history
177 .push(Record::new(self.world.clone(), "wand many"));
178 }
179 if new_annos && let Some(active_tool_name) = find_active_tool(&self.tools) {
180 self.world
181 .request_redraw_annotations(active_tool_name, Visibility::All);
182 }
183
184 self.world.data.meta_data.ssh_cfg = Some(self.ctrl.cfg.ssh_cfg());
185 if project_loaded_in_curr_iter {
186 for t in &mut self.tools {
187 self.world = t.deactivate(mem::take(&mut self.world));
188 }
189 }
190 if let Some(elf) = &self.ctrl.log_export_path {
191 trace_ok_err(self.ctrl.export_logs(elf));
192 }
193 if self.ctrl.log_export_path.is_some() {
194 self.ctrl.log_export_path = None;
195 }
196 if e.held_ctrl() && e.pressed(KeyCode::S) {
197 let prj_path = self.ctrl.cfg.current_prj_path().to_path_buf();
198 if let Err(e) = self
199 .ctrl
200 .save(prj_path, &self.world.data.tools_data_map, true)
201 {
202 self.menu
203 .show_info(Info::Error(format!("could not save project due to {e:?}")));
204 }
205 }
206 });
207
208 egui::Panel::right("my_panel")
209 .show(ui, |ui| {
210 ui.vertical(|ui| {
211 self.tools_select_menu.ui(
212 ui,
213 &mut self.tools,
214 &mut self.world.data.tools_data_map,
215 )
216 })
217 .inner
218 })
219 .inner?;
220
221 if self.recently_clicked_tool_idx.is_none() {
223 self.recently_clicked_tool_idx = self.tools_select_menu.recently_clicked_tool();
224 }
225 if let (Some(idx_active), Some(_)) = (
226 self.recently_clicked_tool_idx,
227 &self.world.data.meta_data.file_path_absolute(),
228 ) && !self.ctrl.flags().is_loading_screen_active
229 {
230 for (i, t) in self.tools.iter_mut().enumerate() {
232 if i != idx_active && t.is_active() && !t.is_always_active() {
233 let meta_data = self.ctrl.meta_data(
234 self.ctrl.file_selected_idx,
235 Some(self.ctrl.flags().is_loading_screen_active),
236 );
237 self.world.data.meta_data = meta_data;
238 self.world = t.deactivate(mem::take(&mut self.world));
239 }
240 }
241 for (i, t) in self.tools.iter_mut().enumerate() {
242 if i == idx_active {
243 (self.world, self.history) =
244 t.activate(mem::take(&mut self.world), mem::take(&mut self.history));
245 }
246 }
247 self.recently_clicked_tool_idx = None;
248 }
249
250 if e.held_alt() && e.pressed(KeyCode::Q) {
251 info!("deactivate all tools");
252 let was_any_tool_active = self
253 .tools
254 .iter()
255 .any(|t| t.is_active() && !t.is_always_active());
256 for t in self.tools.iter_mut() {
257 if !t.is_always_active() && t.is_active() {
258 let meta_data = self.ctrl.meta_data(
259 self.ctrl.file_selected_idx,
260 Some(self.ctrl.flags().is_loading_screen_active),
261 );
262 self.world.data.meta_data = meta_data;
263 self.world = t.deactivate(mem::take(&mut self.world));
264 }
265 }
266 if was_any_tool_active {
267 self.history
268 .push(Record::new(self.world.clone(), "deactivation of all tools"));
269 }
270 }
271 activate_tool_event!(B, BBOX_NAME, e, self.recently_clicked_tool_idx, self.tools);
273 activate_tool_event!(Z, ZOOM_NAME, e, self.recently_clicked_tool_idx, self.tools);
274
275 const DOUBLE_SKIP_TH_MS: u128 = 500;
276 if e.held_ctrl() && e.pressed(KeyCode::M) {
277 self.menu.toggle();
278 } else if e.released(KeyCode::F5) {
279 if let Err(e) = self.ctrl.reload(None) {
280 self.menu
281 .show_info(Info::Error(format!("could not reload due to {e:?}")));
282 }
283 } else if e.held(KeyCode::PageDown) || e.held(KeyCode::PageUp) {
284 if self.world.data.meta_data.flags.is_loading_screen_active == Some(true) {
285 self.next_image_held_timer = Instant::now();
286 } else {
287 let elapsed = self.next_image_held_timer.elapsed().as_millis();
288 let interval = self.ctrl.cfg.usr.image_change_delay_on_held_key_ms as u128;
289 if elapsed > interval {
290 if e.held(KeyCode::PageDown) {
291 self.ctrl.paths_navigator.next();
292 } else if e.held(KeyCode::PageUp) {
293 self.ctrl.paths_navigator.prev();
294 }
295 self.next_image_held_timer = Instant::now();
296 }
297 }
298 } else if e.released(KeyCode::PageDown)
299 && self.next_image_held_timer.elapsed().as_millis() > DOUBLE_SKIP_TH_MS
300 {
301 self.ctrl.paths_navigator.next();
302 } else if e.released(KeyCode::PageUp)
303 && self.next_image_held_timer.elapsed().as_millis() > DOUBLE_SKIP_TH_MS
304 {
305 self.ctrl.paths_navigator.prev();
306 } else if e.released(KeyCode::Escape) {
307 self.world.set_zoom_box(None);
308 }
309
310 if let Some(file_label) = request_file_label_to_load {
312 self.ctrl.paths_navigator.select_file_label(file_label);
313 self.ctrl
314 .paths_navigator
315 .activate_scroll_to_selected_label();
316 }
317 let rx_match = &self.rx_from_http.as_ref().map(|rx| rx.try_iter().last());
319 if let Some(Some(Ok(file_label))) = rx_match {
320 self.ctrl.paths_navigator.select_file_label(file_label);
321 self.ctrl
322 .paths_navigator
323 .activate_scroll_to_selected_label();
324 } else if let Some(Some(Err(e))) = rx_match {
325 warn!("{e:?}");
327 (self.http_addr, self.rx_from_http) =
328 match httpserver::restart_with_increased_port(&self.http_addr) {
329 Ok(x) => x,
330 Err(e) => {
331 error!("{e:?}");
332 (self.http_addr.to_string(), None)
333 }
334 };
335 }
336
337 let world_idx_pair = measure_time!("load image", {
338 for t in &mut self.tools {
341 if t.is_active() {
342 self.world = t.update(mem::take(&mut self.world));
343 }
344 }
345 if e.held_ctrl() && e.pressed(KeyCode::Z) {
347 info!("undo");
348 self.ctrl.undo(&mut self.history)
349 } else if e.held_ctrl() && e.pressed(KeyCode::Y) {
350 info!("redo");
351 self.ctrl.redo(&mut self.history)
352 } else {
353 match measure_time!(
355 "load if",
356 self.ctrl
357 .load_new_image_if_triggered(&self.world, &mut self.history)
358 ) {
359 Ok(iip) => iip,
360 Err(e) => {
361 measure_time!(
362 "show info",
363 self.menu.show_info(Info::Error(format!("{e:?}")))
364 );
365 None
366 }
367 }
368 }
369 });
370
371 if let Some((world, file_label_idx)) = world_idx_pair {
372 self.world = world;
373 if let Some(active_tool_name) = find_active_tool(&self.tools) {
374 self.world
375 .request_redraw_annotations(active_tool_name, Visibility::All);
376 }
377 if file_label_idx.is_some() {
378 self.ctrl.paths_navigator.select_label_idx(file_label_idx);
379 let meta_data = self.ctrl.meta_data(
380 self.ctrl.file_selected_idx,
381 Some(self.ctrl.flags().is_loading_screen_active),
382 );
383 self.world.data.meta_data = meta_data;
384 if !self.ctrl.flags().is_loading_screen_active {
385 for t in &mut self.tools {
386 if t.is_active() {
387 (self.world, self.history) = t.file_changed(
388 mem::take(&mut self.world),
389 mem::take(&mut self.history),
390 );
391 }
392 }
393 }
394 }
395 }
396
397 if are_tools_active(&self.menu, &self.tools_select_menu) {
398 let meta_data = self.ctrl.meta_data(
399 self.ctrl.file_selected_idx,
400 Some(self.ctrl.flags().is_loading_screen_active),
401 );
402 self.world.data.meta_data = meta_data;
403 (self.world, self.history) = apply_tools(
404 &mut self.tools,
405 mem::take(&mut self.world),
406 mem::take(&mut self.history),
407 e,
408 );
409 }
410
411 if let Some(idx) = self.ctrl.paths_navigator.file_label_selected_idx() {
413 let pixel_pos = e.mouse_pos_on_orig.map(|mp| mp.into());
414 let data_point = get_pixel_on_orig_str(&self.world, &pixel_pos);
415 let shape = self.world.shape_orig();
416 let file_label = self.ctrl.file_label(idx);
417 let active_tool = self.tools.iter().find(|t| t.is_active());
418 let tool_string = if let Some(t) = active_tool {
419 format!("{} tool is active", t.name)
420 } else {
421 "".to_string()
422 };
423 let zoom_box_coords = self
424 .world
425 .zoom_box()
426 .map(|zb| {
427 let zb = BbI::from(zb);
428 format!("zoom x {}, y {}, w {}, h {}", zb.x, zb.y, zb.w, zb.h)
429 })
430 .unwrap_or("no zoom".into());
431 let s = match data_point {
432 Some(s) => ImageInfo {
433 filename: file_label.to_string(),
434 shape_info: format!("{}x{}", shape.w, shape.h),
435 pixel_value: s,
436 tool_info: tool_string,
437 zoom_box_coords,
438 },
439 None => ImageInfo {
440 filename: file_label.to_string(),
441 shape_info: format!("{}x{}", shape.w, shape.h),
442 pixel_value: "(x, y) -> (r, g, b)".to_string(),
443 tool_info: tool_string,
444 zoom_box_coords,
445 },
446 };
447 self.world.update_view.image_info = Some(s);
448 }
449 if let Some(n_autosaves) = self.ctrl.cfg.usr.n_autosaves
450 && self.autosave_timer.elapsed().as_secs() > AUTOSAVE_INTERVAL_S
451 {
452 self.autosave_timer = Instant::now();
453 let homefolder = self.ctrl.cfg.home_folder().to_string();
454 let current_prj_path = self.ctrl.cfg.current_prj_path().to_path_buf();
455 let save_prj = |prj_path| {
456 self.ctrl
457 .save(prj_path, &self.world.data.tools_data_map, false)
458 };
459 trace_ok_err(autosave(
460 ¤t_prj_path,
461 homefolder,
462 n_autosaves,
463 save_prj,
464 ));
465 }
466
467 Ok((
468 mem::take(&mut self.world.update_view),
469 self.ctrl.cfg.usr.show_main_image(),
470 self.ctrl.cfg.usr.show_thumbs(),
471 get_prj_name(self.ctrl.cfg.current_prj_path(), None),
472 ))
473 })
474 }
475 pub fn load_prj_during_startup(&mut self, file_path: Option<PathBuf>) -> RvResult<()> {
476 if let Some(file_path) = file_path {
477 info!("loaded project {file_path:?}");
478 self.world.data.tools_data_map = self.ctrl.load(file_path)?;
479 } else {
480 let pp = self.ctrl.cfg.current_prj_path().to_path_buf();
481 match self.ctrl.load(pp) {
483 Ok(td) => {
484 info!(
485 "loaded last saved project {:?}",
486 self.ctrl.cfg.current_prj_path()
487 );
488 self.world.data.tools_data_map = td;
489 }
490 Err(e) => {
491 if DEFAULT_PRJ_PATH.as_os_str() != self.ctrl.cfg.current_prj_path().as_os_str()
492 {
493 info!(
494 "could not read last saved project {:?} due to {e:?} ",
495 self.ctrl.cfg.current_prj_path()
496 );
497 }
498 }
499 }
500 }
501 Ok(())
502 }
503 pub fn import_prj(&mut self, file_path: &Path) -> RvResult<()> {
504 self.world.data.tools_data_map = self.ctrl.replace_with_save(file_path)?;
505 Ok(())
506 }
507}