Skip to main content

maps/app_impl/
central_panel.rs

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        // Handle input interaction and adapt mouse pointer to the active tool.
82        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        // Note: the updated grid options are used in the next frame.
93        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            // Don't show the other fixed lenses too not get too messy.
123            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            // Don't show fixed lenses when measuring.
143            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                    // Offset each new lens window a bit.
172                    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            // Show the lens grid.
202            // Crop threshold is set to 0 to always crop the textures in a lens.
203            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            // Always fill the lens window with a background rectangle.
207            // Ensure that the lens uses the same background color as the main grid canvas.
208            mini_grid.draw_background(self.options.canvas_settings.background_color);
209            // Only show actual data if the center is set (can be None when hover lens loses focus).
210            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            // The "classic" lens is not shown in aligned mode, we add grids there.
237            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_load_screen(&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                        if self.data.maps.is_empty() {
265                            ui.heading("No maps loaded.");
266                        } else {
267                            ui.heading("Select a view in the top bar or load more maps.");
268                        }
269                        ui.add_space(2. * SPACE);
270                        self.load_meta_button(ui);
271                        ui.add_space(SPACE);
272
273                        #[cfg(not(target_arch = "wasm32"))]
274                        self.load_session_button(ui);
275                        #[cfg(target_arch = "wasm32")]
276                        ui.add_enabled_ui(false, |ui| {
277                            self.load_session_button(ui);
278                        });
279
280                        #[cfg(target_arch = "wasm32")]
281                        {
282                            ui.add_space(SPACE * 3.);
283                            ui.label(
284                                egui::RichText::new(
285                                    "Filesystem IO is limited in the web assembly app.",
286                                )
287                                .color(egui::Color32::ORANGE),
288                            );
289                            ui.add(
290                                egui::Hyperlink::from_label_and_url(
291                                    "Click here to learn more.",
292                                    "https://github.com/MichaelGrupp/maps?tab=readme-ov-file#maps",
293                                )
294                                .open_in_new_tab(true),
295                            );
296                            ui.add_space(5. * SPACE);
297                            self.demo_buttons(ui);
298                        }
299                    });
300                });
301            },
302        );
303    }
304
305    /// Central panel that shows the map content.
306    /// Returns the rect of the viewport for screenshot purposes.
307    pub(crate) fn central_panel(&mut self, ui: &mut egui::Ui) -> egui::Rect {
308        let mut viewport_rect = egui::Rect::ZERO;
309
310        egui::CentralPanel::default()
311            .frame(egui::Frame::default().fill(self.options.canvas_settings.background_color))
312            .show(ui.ctx(), |ui| {
313                viewport_rect = ui.clip_rect();
314
315                if self.data.maps.is_empty() {
316                    self.options.view_mode = ViewMode::LoadScreen;
317                }
318
319                match self.options.view_mode {
320                    ViewMode::Tiles => {
321                        self.show_tiles(ui);
322                    }
323                    ViewMode::Stacked => {
324                        egui::ScrollArea::both().show(ui, |ui| {
325                            self.show_stacked_images(ui);
326                            // Fill the remaining vertical space, otherwise the scroll bar can jump around.
327                            ui.add_space(ui.available_height());
328                        });
329                    }
330                    ViewMode::Aligned => {
331                        self.show_grid(ui);
332                    }
333                    ViewMode::LoadScreen => {
334                        self.show_load_screen(ui);
335                    }
336                }
337            });
338
339        viewport_rect
340    }
341}