rvlib/
main_loop.rs

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        // http server state
149        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            // tool activation
226            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                    // first deactivate, then activate
235                    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            // tool activation keyboard shortcuts
277            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            // check for new image requests from http server
316            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                // if the server thread sends an error we restart the server
324                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                // load new image if requested by a menu click or by the http server
337                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                    // let mut world = measure_time!("world clone", self.world.clone());
345                    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            // show position and rgb value
401            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                        &current_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            // load last project
459            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}