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