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