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