1use eframe::egui;
2use log::{debug, error};
3use uuid::Uuid;
4
5use crate::app::{ActiveTool, AppState, ViewMode};
6use crate::app_impl::constants::SPACE;
7use crate::grid::Grid;
8use crate::grid_options::{LineType, SubLineVisibility};
9use crate::lens::Lens;
10use crate::tiles_behavior::MapsTreeBehavior;
11use maps_rendering::TextureRequest;
12
13const STACKED_TEXTURE_ID: &str = "stack";
14
15impl AppState {
16 fn show_tiles(&mut self, ui: &mut egui::Ui) {
17 let hovered_id = {
18 let mut behavior = MapsTreeBehavior {
19 maps: &mut self.data.maps,
20 hovered_id: None,
21 };
22 self.tile_manager.tree.ui(&mut behavior, ui);
23 behavior.hovered_id
24 };
25
26 if let Some(hovered_id) = hovered_id {
27 self.show_lens(ui, &hovered_id, &hovered_id);
28 } else {
29 self.status.active_tool = None;
30 }
31 }
32
33 fn show_stacked_images(&mut self, ui: &mut egui::Ui) {
34 let num_visible = self.data.maps.values().filter(|m| m.visible).count();
35 let rect_per_image = egui::Rect::from_min_max(
36 egui::Pos2::ZERO,
37 egui::pos2(
38 ui.available_width(),
39 ui.available_height() / num_visible as f32,
40 ) * self.options.canvas_settings.stack_scale_factor,
41 );
42 self.status.active_tool = None;
43 for name in self.data.draw_order.keys() {
44 let Some(map) = self.data.maps.get_mut(name) else {
45 error!("Unknown draw order key: {name}");
46 continue;
47 };
48
49 if !map.visible {
50 continue;
51 }
52 ui.with_layout(egui::Layout::top_down(egui::Align::TOP), |ui| {
53 let request = &TextureRequest::new(name.clone(), rect_per_image)
54 .with_tint(map.tint)
55 .with_color_to_alpha(map.color_to_alpha)
56 .with_thresholding(map.get_value_interpretation())
57 .with_texture_options(map.texture_filter.to_egui());
58 map.get_or_create_texture_state(STACKED_TEXTURE_ID)
59 .put(ui, request);
60 if let Some(response) = &map
61 .get_or_create_texture_state(STACKED_TEXTURE_ID)
62 .image_response
63 && response.hovered()
64 {
65 self.status.active_tool = Some(name.clone());
66 }
67 });
68 }
69 if let Some(hovered_map) = &self.status.active_tool {
70 self.show_lens(ui, hovered_map.clone().as_str(), STACKED_TEXTURE_ID);
71 }
72 }
73
74 fn show_grid(&mut self, ui: &mut egui::Ui) {
75 let options = &mut self.options.grid;
76
77 let grid = Grid::new(ui, "main_grid", options.scale)
78 .with_origin_offset(options.offset)
79 .with_texture_crop_threshold(self.options.advanced.grid_crop_threshold);
80
81 if grid.response().hovered() {
83 match self.options.active_tool {
84 ActiveTool::PlaceLens | ActiveTool::Measure | ActiveTool::HoverLens => {
85 ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
86 }
87 _ => {
88 ui.ctx().set_cursor_icon(egui::CursorIcon::Default);
89 }
90 }
91 }
92 grid.update_drag_and_zoom(ui, options);
94
95 grid.show_maps(ui, &mut self.data.maps, options, &self.data.draw_order);
96 if options.lines_visible {
97 grid.draw_lines(options, &LineType::Main);
98 }
99 if options.sub_lines_visible == SubLineVisibility::Always {
100 grid.draw_lines(options, &LineType::Sub);
101 }
102 if options.marker_visibility.zero_visible() {
103 grid.draw_axes(options, None);
104 }
105 self.status.hover_position = grid.hover_pos_metric();
106 if let Some(pos) = self.status.hover_position
107 && ui.input(|i| i.events.contains(&egui::Event::Copy))
108 {
109 ui.ctx()
110 .copy_text(format!("{{x: {:.2}, y: {:.2}, z: 0}}", pos.x, pos.y));
111 }
112
113 if self.options.active_tool == ActiveTool::None {
114 self.status.active_tool = None;
115 }
116 if self.options.active_tool == ActiveTool::HoverLens {
117 self.status.active_tool =
118 Some(format!("🔍 {}x magnification", options.lens_magnification));
119 }
120 if self.options.active_tool == ActiveTool::HoverLens {
121 self.show_grid_lens(ui, self.status.hover_position, "hover_lens", false, None);
122 return;
124 }
125
126 if self.options.active_tool == ActiveTool::Measure {
127 self.status.active_tool = Some("📏 Measurement tool active".to_string());
128 if !grid.response().clicked() {
129 grid.draw_measure(options, self.status.hover_position);
130 return;
131 }
132 if let Some(click_pos) = self.status.hover_position {
133 if options.measure_start.is_none() {
134 options.measure_start = Some(click_pos);
135 } else if options.measure_end.is_none() {
136 options.measure_end = Some(click_pos);
137 } else {
138 options.measure_start = Some(click_pos);
139 options.measure_end = None;
140 }
141 }
142 return;
144 }
145
146 if grid.response().clicked()
147 && self.options.active_tool == ActiveTool::PlaceLens
148 && let Some(pos) = self.status.hover_position
149 {
150 let id = Uuid::new_v4().to_string();
151 debug!("Placing lens {id} focussing {pos:?}.");
152 self.data.grid_lenses.insert(id, pos);
153 self.status.unsaved_changes = true;
154 self.options.active_tool = ActiveTool::None;
155 }
156 let lens_ids = self.data.grid_lenses.keys().cloned().collect::<Vec<_>>();
157 if self.options.active_tool == ActiveTool::PlaceLens || !lens_ids.is_empty() {
158 self.status.active_tool = Some(format!(
159 "🔍 {} fixed lenses active at {}x magnification",
160 self.data.grid_lenses.len(),
161 options.lens_magnification
162 ));
163 }
164 for (i, lens_id) in lens_ids.iter().enumerate() {
165 if let Some(pos) = self.data.grid_lenses.get(lens_id) {
166 self.show_grid_lens(
167 ui,
168 Some(*pos),
169 lens_id.clone().as_str(),
170 true,
171 Some(i as f32 * egui::vec2(20., 20.)),
173 );
174 }
175 }
176 }
177
178 pub(crate) fn show_grid_lens(
179 &mut self,
180 ui: &mut egui::Ui,
181 center_pos: Option<egui::Pos2>,
182 id: &str,
183 closable: bool,
184 default_offset: Option<egui::Vec2>,
185 ) {
186 let options = &self.options.grid;
187 let grid_lens_scale = options.scale * options.lens_magnification;
188 let mut open = true;
189 let mut window = egui::Window::new(egui::RichText::new("🔍").strong())
190 .title_bar(true)
191 .id(egui::Id::new(id))
192 .auto_sized()
193 .resizable(true)
194 .collapsible(true)
195 .default_size(egui::vec2(250., 250.))
196 .default_pos(ui.clip_rect().min + default_offset.unwrap_or(egui::vec2(0., 0.)));
197 if closable {
198 window = window.open(&mut open);
199 }
200 window.show(ui.ctx(), |ui| {
201 let mini_grid = Grid::new(ui, id, grid_lens_scale)
204 .centered_at(center_pos.unwrap_or_default())
205 .with_texture_crop_threshold(0);
206 mini_grid.draw_background(self.options.canvas_settings.background_color);
209 if center_pos.is_some() {
211 mini_grid.show_maps(ui, &mut self.data.maps, options, &self.data.draw_order);
212 if options.lines_visible {
213 mini_grid.draw_lines(options, &LineType::Main);
214 }
215 if options.sub_lines_visible == SubLineVisibility::Always
216 || options.sub_lines_visible == SubLineVisibility::OnlyLens
217 {
218 mini_grid.draw_lines(options, &LineType::Sub);
219 }
220 if options.marker_visibility.zero_visible() {
221 mini_grid.draw_axes(options, None);
222 }
223 }
224 });
225 if !open {
226 self.data.grid_lenses.remove(id);
227 for (name, map) in self.data.maps.iter_mut() {
228 debug!("Removing lens texture state with ID {id} from map {name}.");
229 map.texture_states.remove(id);
230 }
231 }
232 }
233
234 fn show_lens(&mut self, ui: &mut egui::Ui, map_id: &str, texture_id: &str) {
235 if self.options.view_mode == ViewMode::Aligned {
236 return;
238 }
239 if self.options.active_tool != ActiveTool::HoverLens {
240 self.status.active_tool = None;
241 return;
242 }
243
244 if let Some(map) = self.data.maps.get_mut(map_id)
245 && Lens::with(&mut self.options.lens).show_on_hover(
246 ui,
247 map,
248 texture_id,
249 &self.options.canvas_settings,
250 )
251 {
252 self.status.active_tool = Some(map_id.to_string());
253 }
254 }
255
256 fn show_empty(&mut self, ui: &mut egui::Ui) {
257 ui.with_layout(
258 egui::Layout::centered_and_justified(egui::Direction::TopDown),
259 |ui| {
260 ui.horizontal_centered(|ui| {
261 ui.vertical_centered(|ui| {
262 let frac = if cfg!(target_arch = "wasm32") { 4. } else { 2. };
263 ui.add_space((ui.available_height() / frac - 100.).max(SPACE));
264 ui.heading("No maps loaded.");
265 ui.add_space(2. * SPACE);
266 self.load_meta_button(ui);
267 ui.add_space(SPACE);
268
269 #[cfg(not(target_arch = "wasm32"))]
270 self.load_session_button(ui);
271 #[cfg(target_arch = "wasm32")]
272 ui.add_enabled_ui(false, |ui| {
273 self.load_session_button(ui);
274 });
275
276 #[cfg(target_arch = "wasm32")]
277 {
278 ui.add_space(SPACE * 3.);
279 ui.label(
280 egui::RichText::new(
281 "Filesystem IO is limited in the web assembly app.",
282 )
283 .color(egui::Color32::ORANGE),
284 );
285 ui.add(
286 egui::Hyperlink::from_label_and_url(
287 "Click here to learn more.",
288 "https://github.com/MichaelGrupp/maps?tab=readme-ov-file#maps",
289 )
290 .open_in_new_tab(true),
291 );
292 ui.add_space(5. * SPACE);
293 self.demo_buttons(ui);
294 }
295 });
296 });
297 },
298 );
299 }
300
301 pub(crate) fn central_panel(&mut self, ui: &mut egui::Ui) -> egui::Rect {
304 let mut viewport_rect = egui::Rect::ZERO;
305
306 egui::CentralPanel::default()
307 .frame(egui::Frame::default().fill(self.options.canvas_settings.background_color))
308 .show(ui.ctx(), |ui| {
309 viewport_rect = ui.clip_rect();
310
311 if self.data.maps.is_empty() {
312 self.show_empty(ui);
313 return;
314 }
315
316 match self.options.view_mode {
317 ViewMode::Tiles => {
318 self.show_tiles(ui);
319 }
320 ViewMode::Stacked => {
321 egui::ScrollArea::both().show(ui, |ui| {
322 self.show_stacked_images(ui);
323 ui.add_space(ui.available_height());
325 });
326 }
327 ViewMode::Aligned => {
328 self.show_grid(ui);
329 }
330 }
331 });
332
333 viewport_rect
334 }
335}