diff --git a/src/main.rs b/src/main.rs index ebcd20b6..e7848813 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,50 @@ use egui::{ epaint::RectShape, Color32, ColorImage, Context, Image, Pos2, Rect, Rounding, Sense, Shape, Stroke, TextureHandle, TextureOptions, Ui, Vec2, }; -use rvlib::{domain::Point, Events, MainEventLoop, UpdateImage}; +use rvlib::{domain::Point, Events, KeyCode, MainEventLoop, UpdateImage}; use std::mem; +fn map_key(egui_key: egui::Key) -> Option { + match egui_key { + egui::Key::A => Some(rvlib::KeyCode::A), + egui::Key::B => Some(rvlib::KeyCode::B), + egui::Key::C => Some(rvlib::KeyCode::C), + egui::Key::D => Some(rvlib::KeyCode::D), + egui::Key::L => Some(rvlib::KeyCode::L), + egui::Key::H => Some(rvlib::KeyCode::H), + egui::Key::M => Some(rvlib::KeyCode::M), + egui::Key::Q => Some(rvlib::KeyCode::Q), + egui::Key::R => Some(rvlib::KeyCode::R), + egui::Key::T => Some(rvlib::KeyCode::T), + egui::Key::V => Some(rvlib::KeyCode::V), + egui::Key::Y => Some(rvlib::KeyCode::Y), + egui::Key::Z => Some(rvlib::KeyCode::Z), + egui::Key::Num0 => Some(rvlib::KeyCode::Key0), + egui::Key::Num1 => Some(rvlib::KeyCode::Key1), + egui::Key::Num2 => Some(rvlib::KeyCode::Key2), + egui::Key::Num3 => Some(rvlib::KeyCode::Key3), + egui::Key::Num4 => Some(rvlib::KeyCode::Key4), + egui::Key::Num5 => Some(rvlib::KeyCode::Key5), + egui::Key::Num6 => Some(rvlib::KeyCode::Key6), + egui::Key::Num7 => Some(rvlib::KeyCode::Key7), + egui::Key::Num8 => Some(rvlib::KeyCode::Key8), + egui::Key::Num9 => Some(rvlib::KeyCode::Key9), + egui::Key::PlusEquals => Some(rvlib::KeyCode::Equals), + egui::Key::Minus => Some(rvlib::KeyCode::Minus), + egui::Key::Delete => Some(rvlib::KeyCode::Delete), + egui::Key::Backspace => Some(rvlib::KeyCode::Back), + egui::Key::ArrowLeft => Some(rvlib::KeyCode::Left), + egui::Key::ArrowRight => Some(rvlib::KeyCode::Right), + egui::Key::ArrowUp => Some(rvlib::KeyCode::Up), + egui::Key::ArrowDown => Some(rvlib::KeyCode::Down), + egui::Key::F5 => Some(rvlib::KeyCode::F5), + egui::Key::PageDown => Some(rvlib::KeyCode::PageDown), + egui::Key::PageUp => Some(rvlib::KeyCode::PageUp), + egui::Key::Escape => Some(rvlib::KeyCode::Escape), + _ => None, + } +} + #[derive(Default)] struct RvImageApp { event_loop: MainEventLoop, @@ -61,8 +102,36 @@ fn draw_bbs(ui: &mut Ui, bbs: &[rvlib::BB], stroke: rvlib::Stroke, fill_rgb: [u8 impl eframe::App for RvImageApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + let update_view = self.event_loop.one_iteration(&self.events, ctx); egui::CentralPanel::default().show(ctx, |ui| { - if let Ok(update_view) = self.event_loop.one_iteration(&self.events, ctx) { + ui.input(|i| { + self.events.set_events( + i.events + .iter() + .flat_map(move |e| match e { + egui::Event::Key { + key, + pressed, + repeat, + modifiers, + } => { + if let Some(k) = map_key(*key) { + if !pressed { + Some(rvlib::Event::Released(k)) + } else { + Some(rvlib::Event::Pressed(k)) + } + + } else { + None + } + } + _ => None, + }) + .collect::>(), + ); + }); + if let Ok(update_view) = update_view { ui.label(update_view.image_info); if let UpdateImage::Yes(im) = update_view.image { let color_image = ColorImage::from_rgb( @@ -86,8 +155,17 @@ impl eframe::App for RvImageApp { let mouse_pos = image_response.hover_pos(); let mouse_pos = mouse_pos.map(|mp| Point { x: ((mp.x - offset_x) / size.x * self.size[0] as f32) as u32, - y: ((mp.y -offset_y) / size.y * self.size[1] as f32) as u32, + y: ((mp.y - offset_y) / size.y * self.size[1] as f32) as u32, }); + if image_response.clicked() { + self.events.released(KeyCode::MouseLeft); + } + if image_response.drag_released() { + self.events.released(KeyCode::MouseLeft); + } + if image_response.drag_started() { + self.events.pressed(KeyCode::MouseLeft); + } self.events = mem::take(&mut self.events).mousepos(mouse_pos); } } diff --git a/src/rvlib/events.rs b/src/rvlib/events.rs index ce7f7106..595cbc5d 100644 --- a/src/rvlib/events.rs +++ b/src/rvlib/events.rs @@ -42,9 +42,8 @@ impl Events { self.mouse_pos = mouse_pos; self } - pub fn events(mut self, events: Vec) -> Self { + pub fn set_events(&mut self, events: Vec) { self.events = events; - self } action_keycode!(held_alt, Held, Alt); action_keycode!(held_shift, Held, Shift); diff --git a/src/rvlib/lib.rs b/src/rvlib/lib.rs index 222c646c..1f1f6fa5 100644 --- a/src/rvlib/lib.rs +++ b/src/rvlib/lib.rs @@ -22,6 +22,6 @@ mod util; pub mod world; pub use domain::BB; pub use drawme::{Stroke, UpdateAnnos, UpdateImage, UpdateView, UpdateZoomBox}; -pub use events::Events; +pub use events::{Events, KeyCode, Event}; pub use main_loop::MainEventLoop; pub use tools_data::annotations; diff --git a/src/rvlib/main_loop.rs b/src/rvlib/main_loop.rs index 96c2bc97..aab69c7c 100644 --- a/src/rvlib/main_loop.rs +++ b/src/rvlib/main_loop.rs @@ -12,7 +12,7 @@ use crate::result::RvResult; use crate::tools::{make_tool_vec, Manipulate, ToolState, ToolWrapper, BBOX_NAME, ZOOM_NAME}; use crate::world::World; use crate::{apply_tool_method_mut, httpserver, image_util, UpdateView}; -use egui::Context; +use egui::{Context, Ui}; use image::{DynamicImage, GenericImageView}; use image::{ImageBuffer, Rgb}; use lazy_static::lazy_static; @@ -57,7 +57,7 @@ fn pos_2_string(im: &DynamicImage, x: u32, y: u32) -> String { ) } else { "".to_string() - } + } } fn get_pixel_on_orig_str(world: &World, mouse_pos: &Option) -> Option { @@ -157,9 +157,19 @@ impl Default for MainEventLoop { impl MainEventLoop { pub fn one_iteration(&mut self, e: &Events, ctx: &Context) -> RvResult { self.menu - .ui(&ctx, &mut self.ctrl, &mut self.world.data.tools_data_map); - self.tools_select_menu - .ui(&ctx, &mut self.tools, &mut self.world.data.tools_data_map)?; + .ui(ctx, &mut self.ctrl, &mut self.world.data.tools_data_map); + egui::SidePanel::right("my_panel") + .show(ctx, |ui| { + ui.vertical(|ui| { + self.tools_select_menu.ui( + ui, + &mut self.tools, + &mut self.world.data.tools_data_map, + ) + }) + .inner + }) + .inner?; // update world based on tools if self.recently_activated_tool_idx.is_none() { diff --git a/src/rvlib/menu/core.rs b/src/rvlib/menu/core.rs index a3bd2df9..dabba5c7 100644 --- a/src/rvlib/menu/core.rs +++ b/src/rvlib/menu/core.rs @@ -9,7 +9,7 @@ use crate::{ tools_data::ToolSpecifics, world::ToolsDataMap, }; -use egui::{Context, Id, Pos2, Response, Ui}; +use egui::{Context, Id, Response, Ui}; use std::mem; use super::tools_menus::bbox_menu; @@ -20,11 +20,11 @@ fn show_popup( icon: &str, popup_id: Id, info_message: Info, - below_respone: &Response, + response: &Response, ) -> Info { ui.memory_mut(|m| m.open_popup(popup_id)); let mut new_msg = Info::None; - egui::popup_below_widget(ui, popup_id, below_respone, |ui| { + egui::popup_above_or_below_widget(ui, popup_id, response, egui::AboveOrBelow::Above, |ui| { let max_msg_len = 500; let shortened_msg = if msg.len() > max_msg_len { &msg[..max_msg_len] @@ -85,35 +85,24 @@ impl ToolSelectMenu { } pub fn ui( &mut self, - ctx: &Context, + ui: &mut Ui, tools: &mut [ToolState], tools_menu_map: &mut ToolsDataMap, ) -> RvResult<()> { - let window_response = egui::Window::new("tools") - .vscroll(true) - .title_bar(false) - .open(&mut self.window_open) - .default_pos(Pos2 { x: 500.0, y: 15.0 }) - .show(ctx, |ui| -> RvResult<()> { - ui.horizontal_top(|ui| { - self.recently_activated_tool = tools - .iter_mut() - .enumerate() - .filter(|(_, t)| !t.is_always_active()) - .find(|(_, t)| ui.selectable_label(t.is_active(), t.button_label).clicked()) - .map(|(i, _)| i); - }); - for v in tools_menu_map.values_mut().filter(|v| v.menu_active) { - let tmp = match &mut v.specifics { - ToolSpecifics::Bbox(x) => bbox_menu(ui, v.menu_active, mem::take(x)), - ToolSpecifics::Brush(_) => Ok(mem::take(v)), - }; - *v = tmp?; - } - Ok(()) - }); - if let (Some(wr), Some(pos)) = (window_response, ctx.pointer_latest_pos()) { - self.are_tools_active = !wr.response.rect.expand(5.0).contains(pos); + ui.horizontal_top(|ui| { + self.recently_activated_tool = tools + .iter_mut() + .enumerate() + .filter(|(_, t)| !t.is_always_active()) + .find(|(_, t)| ui.selectable_label(t.is_active(), t.button_label).clicked()) + .map(|(i, _)| i); + }); + for v in tools_menu_map.values_mut().filter(|v| v.menu_active) { + let tmp = match &mut v.specifics { + ToolSpecifics::Bbox(x) => bbox_menu(ui, v.menu_active, mem::take(x)), + ToolSpecifics::Brush(_) => Ok(mem::take(v)), + }; + *v = tmp?; } Ok(()) } @@ -201,184 +190,183 @@ impl Menu { /// Create the UI using egui. pub fn ui(&mut self, ctx: &Context, ctrl: &mut Control, tools_data_map: &mut ToolsDataMap) { - let window_response = egui::Window::new("menu") - .vscroll(true) - .title_bar(false) - .open(&mut self.window_open) - .show(ctx, |ui| { - // Popup for error messages - let popup_id = ui.make_persistent_id("info-popup"); - let r = ui.separator(); - self.info_message = match &self.info_message { - Info::Warning(msg) => { - show_popup(ui, msg, "❕", popup_id, self.info_message.clone(), &r) - } - Info::Error(msg) => { - show_popup(ui, msg, "❌", popup_id, self.info_message.clone(), &r) - } - Info::None => Info::None, - }; - - // Top row with open folder and settings button - ui.horizontal(|ui| { - let button_resp = open_folder::button(ui, ctrl, self.open_folder_popup_open); - handle_error!( - |open| { - self.open_folder_popup_open = open; - }, - button_resp, - self - ); - let popup_id = ui.make_persistent_id("cfg-popup"); - self.load_button_resp.resp = Some(ui.button("load project")); + egui::TopBottomPanel::top("top-menu-bar").show(ctx, |ui| { + // Top row with open folder and settings button + egui::menu::bar(ui, |ui| { + let button_resp = open_folder::button(ui, ctrl, self.open_folder_popup_open); + handle_error!( + |open| { + self.open_folder_popup_open = open; + }, + button_resp, + self + ); + let popup_id = ui.make_persistent_id("cfg-popup"); + self.load_button_resp.resp = Some(ui.button("load project")); - if ui.button("save project").clicked() { - handle_error!(ctrl.save(tools_data_map), self); - } + if ui.button("save project").clicked() { + handle_error!(ctrl.save(tools_data_map), self); + } - let cfg_gui = - CfgMenu::new(popup_id, &mut ctrl.cfg, &mut self.editable_ssh_cfg_str); - ui.add(cfg_gui); - }); + let cfg_gui = CfgMenu::new(popup_id, &mut ctrl.cfg, &mut self.editable_ssh_cfg_str); + ui.add(cfg_gui); + }); + }); - if let Ok(folder) = ctrl.cfg.export_folder() { - if let Some(load_btn_resp) = &self.load_button_resp.resp { - if load_btn_resp.clicked() { - self.load_button_resp.popup_open = true; - } - if self.load_button_resp.popup_open { - let mut filename_for_import = None; - let mut exports = || -> RvResult<()> { - let files = - file_util::files_in_folder(folder, RVPRJ_PREFIX, "json") - .map_err(to_rv)? - .filter_map(|p| { - p.file_name().map(|p| p.to_str().map(|p| p.to_string())) - }) - .flatten() - .collect::>(); - if !files.is_empty() { - filename_for_import = picklist::pick( - ui, - files.iter().map(|s| s.as_str()), - 200.0, - load_btn_resp, - ) - .map(|s| s.to_string()); - } else { - println!("no projects found that can be loaded") - } - Ok(()) - }; - handle_error!(exports(), self); - if let Some(filename) = filename_for_import { - handle_error!( - |tdm| { - *tools_data_map = tdm; - }, - ctrl.load(&filename), - self - ); - self.load_button_resp.resp = None; - self.load_button_resp.popup_open = false; + egui::SidePanel::left("left-main-menu").show(ctx, |ui| { + if let Ok(folder) = ctrl.cfg.export_folder() { + if let Some(load_btn_resp) = &self.load_button_resp.resp { + if load_btn_resp.clicked() { + self.load_button_resp.popup_open = true; + } + if self.load_button_resp.popup_open { + let mut filename_for_import = None; + let mut exports = || -> RvResult<()> { + let files = file_util::files_in_folder(folder, RVPRJ_PREFIX, "json") + .map_err(to_rv)? + .filter_map(|p| { + p.file_name().map(|p| p.to_str().map(|p| p.to_string())) + }) + .flatten() + .collect::>(); + if !files.is_empty() { + filename_for_import = picklist::pick( + ui, + files.iter().map(|s| s.as_str()), + 200.0, + load_btn_resp, + ) + .map(|s| s.to_string()); + } else { + println!("no projects found that can be loaded") } + Ok(()) + }; + handle_error!(exports(), self); + if let Some(filename) = filename_for_import { + handle_error!( + |tdm| { + *tools_data_map = tdm; + }, + ctrl.load(&filename), + self + ); + self.load_button_resp.resp = None; + self.load_button_resp.popup_open = false; } } } - let mut connected = false; + } + let mut connected = false; + handle_error!( + |con| { + connected = con; + }, + ctrl.check_if_connected(), + self + ); + if connected { + ui.label(ctrl.opened_folder_label().unwrap_or("")); + } else { + ui.label("connecting..."); + } + + let filter_txt_field = ui.text_edit_singleline(&mut self.filter_string); + if filter_txt_field.gained_focus() { + self.are_tools_active = false; + } + if filter_txt_field.lost_focus() { + self.are_tools_active = true; + } + if filter_txt_field.changed() { handle_error!( - |con| { - connected = con; - }, - ctrl.check_if_connected(), + ctrl.paths_navigator + .filter(&self.filter_string, tools_data_map), self ); - if connected { - ui.label(ctrl.opened_folder_label().unwrap_or("")); - } else { - ui.label("connecting..."); - } + } + // Popup for error messages + let popup_id = ui.make_persistent_id("info-popup"); + self.info_message = match &self.info_message { + Info::Warning(msg) => show_popup( + ui, + msg, + "❕", + popup_id, + self.info_message.clone(), + &filter_txt_field, + ), + Info::Error(msg) => show_popup( + ui, + msg, + "❌", + popup_id, + self.info_message.clone(), + &filter_txt_field, + ), + Info::None => Info::None, + }; - let filter_txt_field = ui.text_edit_singleline(&mut self.filter_string); - if filter_txt_field.gained_focus() { - self.are_tools_active = false; - } - if filter_txt_field.lost_focus() { - self.are_tools_active = true; - } - if filter_txt_field.changed() { - handle_error!( - ctrl.paths_navigator - .filter(&self.filter_string, tools_data_map), - self - ); + // scroll area showing image file names + let scroll_to_selected = ctrl.paths_navigator.scroll_to_selected_label(); + let mut filtered_label_selected_idx = ctrl.paths_navigator.file_label_selected_idx(); + if let Some(ps) = &ctrl.paths_navigator.paths_selector() { + self.scroll_offset = menu::scroll_area::scroll_area( + ui, + &mut filtered_label_selected_idx, + ps, + ctrl.file_info_selected.as_deref(), + scroll_to_selected, + self.scroll_offset, + ); + ctrl.paths_navigator.deactivate_scroll_to_selected_label(); + if ctrl.paths_navigator.file_label_selected_idx() != filtered_label_selected_idx { + ctrl.paths_navigator + .select_label_idx(filtered_label_selected_idx); } + } - // scroll area showing image file names - let scroll_to_selected = ctrl.paths_navigator.scroll_to_selected_label(); - let mut filtered_label_selected_idx = - ctrl.paths_navigator.file_label_selected_idx(); - if let Some(ps) = &ctrl.paths_navigator.paths_selector() { - self.scroll_offset = menu::scroll_area::scroll_area( - ui, - &mut filtered_label_selected_idx, - ps, - ctrl.file_info_selected.as_deref(), - scroll_to_selected, - self.scroll_offset, - ); - ctrl.paths_navigator.deactivate_scroll_to_selected_label(); - if ctrl.paths_navigator.file_label_selected_idx() != filtered_label_selected_idx - { - ctrl.paths_navigator - .select_label_idx(filtered_label_selected_idx); - } + ui.separator(); + if let Some(info) = &self.stats.n_files_filtered_info { + ui.label(info); + } + if let Some(info) = &self.stats.n_files_annotated_info { + ui.label(info); + } + let get_file_info = |ps: &PathsSelector| { + let n_files_filtered = ps.filtered_idx_file_label_pairs().len(); + Some(format!("{n_files_filtered} files")) + }; + let get_annotation_info = |ps: &PathsSelector| { + if let Some(bbox_data) = tools_data_map.get(BBOX_NAME) { + let n_files_annotated = bbox_data + .specifics + .bbox() + .n_annotated_images(&ps.filtered_file_paths()); + Some(format!("{n_files_annotated} files with bbox annotations")) + } else { + None } - - ui.separator(); - if let Some(info) = &self.stats.n_files_filtered_info { - ui.label(info); + }; + if let Some(ps) = ctrl.paths_navigator.paths_selector() { + if self.stats.n_files_filtered_info.is_none() { + self.stats.n_files_filtered_info = get_file_info(ps); } - if let Some(info) = &self.stats.n_files_annotated_info { - ui.label(info); + if self.stats.n_files_annotated_info.is_none() { + self.stats.n_files_annotated_info = get_annotation_info(ps); } - let get_file_info = |ps: &PathsSelector| { - let n_files_filtered = ps.filtered_idx_file_label_pairs().len(); - Some(format!("{n_files_filtered} files")) - }; - let get_annotation_info = |ps: &PathsSelector| { - if let Some(bbox_data) = tools_data_map.get(BBOX_NAME) { - let n_files_annotated = bbox_data - .specifics - .bbox() - .n_annotated_images(&ps.filtered_file_paths()); - Some(format!("{n_files_annotated} files with bbox annotations")) - } else { - None - } - }; - if let Some(ps) = ctrl.paths_navigator.paths_selector() { - if self.stats.n_files_filtered_info.is_none() { - self.stats.n_files_filtered_info = get_file_info(ps); - } - if self.stats.n_files_annotated_info.is_none() { - self.stats.n_files_annotated_info = get_annotation_info(ps); - } - if ui.button("re-compute stats").clicked() { - self.stats.n_files_filtered_info = get_file_info(ps); - self.stats.n_files_annotated_info = get_annotation_info(ps); - } - } else { - self.stats.n_files_filtered_info = None; - self.stats.n_files_annotated_info = None; + if ui.button("re-compute stats").clicked() { + self.stats.n_files_filtered_info = get_file_info(ps); + self.stats.n_files_annotated_info = get_annotation_info(ps); } + } else { + self.stats.n_files_filtered_info = None; + self.stats.n_files_annotated_info = None; + } - ui.separator(); - ui.hyperlink_to("license and code", "https://github.com/bertiqwerty/rvimage"); - }); - if let (Some(wr), Some(pos)) = (window_response, ctx.pointer_latest_pos()) { - self.are_tools_active = !wr.response.rect.expand(5.0).contains(pos); - } + ui.separator(); + ui.hyperlink_to("license and code", "https://github.com/bertiqwerty/rvimage"); + }); } }