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