paiagram/interface/
tabs.rs1use std::borrow::Cow;
2
3use bevy::ecs::world::World;
4use egui::{Id, Response, Ui, WidgetText};
5use egui_i18n::tr;
6
7use crate::interface::StatusBarState;
8
9pub mod classes;
10pub mod diagram;
11pub mod displayed_lines;
12pub mod graph;
13pub mod inspector;
14pub mod minesweeper;
15pub mod overview;
16pub mod services;
17pub mod settings;
18pub mod start;
19pub mod station_timetable;
20pub mod tree_view;
21pub mod vehicle;
22
23pub mod all_tabs {
24 pub use super::classes::ClassesTab;
25 pub use super::diagram::DiagramTab;
26 pub use super::displayed_lines::DisplayedLinesTab;
27 pub use super::graph::GraphTab;
28 pub use super::inspector::InspectorTab;
29 pub use super::minesweeper::MinesweeperTab;
30 pub use super::overview::OverviewTab;
31 pub use super::services::ServicesTab;
32 pub use super::settings::SettingsTab;
33 pub use super::start::StartTab;
34 pub use super::station_timetable::StationTimetableTab;
35 pub use super::vehicle::VehicleTab;
36}
37
38#[derive(Debug)]
40pub struct PageCache<K, V>
41where
42 K: PartialEq + Ord,
43{
44 keys: Vec<K>,
45 vals: Vec<V>,
46}
47
48const LINEAR_THRESHOLD: usize = 64;
49
50impl<K, V> PageCache<K, V>
51where
52 K: PartialEq + Ord,
53{
54 pub fn get_mut_or_insert_with<F>(&mut self, query_key: K, make_val: F) -> &mut V
56 where
57 F: FnOnce() -> V,
58 {
59 if self.keys.len() < LINEAR_THRESHOLD
60 && let Some(idx) = self.keys.iter().position(|e| *e == query_key)
61 {
62 return &mut self.vals[idx];
63 }
64 return match self.keys.binary_search(&query_key) {
65 Ok(idx) => &mut self.vals[idx],
66 Err(idx) => {
67 self.keys.insert(idx, query_key);
68 self.vals.insert(idx, make_val());
69 return &mut self.vals[idx];
70 }
71 };
72 }
73}
74
75impl<K, V> Default for PageCache<K, V>
76where
77 K: PartialEq + Ord,
78{
79 fn default() -> Self {
80 Self {
81 keys: Vec::new(),
82 vals: Vec::new(),
83 }
84 }
85}
86
87pub trait Navigatable {
88 fn zoom_x(&self) -> f32;
89 fn zoom_y(&self) -> f32;
90 fn set_zoom(&mut self, zoom_x: f32, zoom_y: f32);
91 fn offset_x(&self) -> f64;
92 fn offset_y(&self) -> f32;
93 fn set_offset(&mut self, offset_x: f64, offset_y: f32);
94 fn allow_axis_zoom(&self) -> bool {
95 false
96 }
97 fn handle_navigation(&mut self, ui: &mut Ui, response: &Response) {
98 let zoom_delta = if self.allow_axis_zoom() {
99 ui.input(|input| input.zoom_delta_2d())
100 } else {
101 let zoom_delta = ui.input(|input| input.zoom_delta());
102 egui::vec2(zoom_delta, zoom_delta)
103 };
104 let scroll_delta = ui.input(|input| input.smooth_scroll_delta);
105 let zooming = (zoom_delta.x - 1.0).abs() > 0.001 || (zoom_delta.y - 1.0).abs() > 0.001;
106
107 if zooming
108 && ui.ui_contains_pointer()
109 && let Some(pos) = response.hover_pos()
110 {
111 let old_zoom_x = self.zoom_x();
112 let old_zoom_y = self.zoom_y();
113 let mut new_zoom_x = old_zoom_x * zoom_delta.x;
114 let mut new_zoom_y = old_zoom_y * zoom_delta.y;
115 let (clamped_x, clamped_y) = self.clamp_zoom(new_zoom_x, new_zoom_y);
116 new_zoom_x = clamped_x;
117 new_zoom_y = clamped_y;
118
119 let rel_pos = (pos - response.rect.min) / response.rect.size();
120 let world_width_before = response.rect.width() as f64 / old_zoom_x as f64;
121 let world_width_after = response.rect.width() as f64 / new_zoom_x as f64;
122 let world_pos_before_x = self.offset_x() + rel_pos.x as f64 * world_width_before;
123 let new_offset_x = world_pos_before_x - rel_pos.x as f64 * world_width_after;
124
125 let world_height_before = response.rect.height() as f64 / old_zoom_y as f64;
126 let world_height_after = response.rect.height() as f64 / new_zoom_y as f64;
127 let world_pos_before_y =
128 self.offset_y() as f64 + rel_pos.y as f64 * world_height_before;
129 let new_offset_y = (world_pos_before_y - rel_pos.y as f64 * world_height_after) as f32;
130
131 self.set_zoom(new_zoom_x, new_zoom_y);
132 self.set_offset(new_offset_x, new_offset_y);
133 }
134 if ui.ui_contains_pointer() || ui.input(|r| r.any_touches()) {
135 let ticks_per_screen_unit = 1.0 / self.zoom_x() as f64;
136 let pan_delta = response.drag_delta() + scroll_delta;
137 let new_offset_x = self.offset_x() - ticks_per_screen_unit * pan_delta.x as f64;
138 let new_offset_y = self.offset_y() - pan_delta.y / self.zoom_y();
139 self.set_offset(new_offset_x, new_offset_y);
140 }
141
142 self.post_navigation(response);
143 }
144 fn clamp_zoom(&self, zoom_x: f32, zoom_y: f32) -> (f32, f32) {
145 (zoom_x, zoom_y)
146 }
147 fn post_navigation(&mut self, _response: &Response) {}
148}
149
150pub trait Tab {
151 const NAME: &'static str;
154 fn pre_render(&mut self, _world: &mut World) {}
156 fn post_render(&mut self, _world: &mut World) {}
158 fn main_display(&mut self, world: &mut World, ui: &mut Ui);
160 fn edit_display(&mut self, _world: &mut World, ui: &mut Ui) {
161 ui.label(Self::NAME);
162 ui.label(tr!("side-panel-edit-fallback-1"));
163 ui.label(tr!("side-panel-edit-fallback-2"));
164 }
165 fn display_display(&mut self, _world: &mut World, ui: &mut Ui) {
166 ui.label(Self::NAME);
167 ui.label(tr!("side-panel-details-fallback-1"));
168 ui.label(tr!("side-panel-details-fallback-2"));
169 }
170 fn export_display(&mut self, _world: &mut World, ui: &mut Ui) {
171 ui.label(Self::NAME);
172 ui.label(tr!("side-panel-export-fallback-1"));
173 ui.label(tr!("side-panel-export-fallback-2"));
174 }
175 fn title(&self) -> WidgetText {
176 Self::NAME.into()
177 }
178 fn on_tab_button(&self, world: &mut World, response: &Response) {
179 if response.hovered() {
180 let title_text = self.title();
181 let s = &mut world.resource_mut::<StatusBarState>().tooltip;
182 s.clear();
183 s.push_str(self.icon().as_ref());
184 s.push(' ');
185 s.push_str(title_text.text());
186 }
187 }
188 fn id(&self) -> Id {
189 Id::new(Self::NAME)
190 }
191 fn scroll_bars(&self) -> [bool; 2] {
192 [true; 2]
193 }
194 fn frame(&self) -> egui::Frame {
195 egui::Frame::default().inner_margin(egui::Margin::same(6))
196 }
197 fn icon(&self) -> Cow<'static, str> {
198 "🖳".into()
199 }
200}