Skip to main content

rvlib/
main_loop.rs

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