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::{
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        // http server state
151        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            // tool activation
228            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                    // first deactivate, then activate
237                    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            // tool activation keyboard shortcuts
279            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            // 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                // load new image if requested by a menu click or by the http server
339                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                    // let mut world = measure_time!("world clone", self.world.clone());
347                    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            // show position and rgb value
405            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                        &current_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            // load last project
473            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}