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