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