1mod about;
2mod camera;
3mod tabs;
4
5use bevy::{ecs::system::SystemState, prelude::*};
6use bevy_egui::{EguiContexts, EguiPlugin, EguiPrimaryContextPass, egui};
7use egui_dock::{DockArea, DockState};
8use std::{collections::VecDeque, sync::Arc};
9
10pub struct InterfacePlugin;
12
13impl Plugin for InterfacePlugin {
14 fn build(&self, app: &mut App) {
15 app.add_plugins(EguiPlugin::default())
16 .add_message::<UiCommand>()
17 .add_systems(Startup, camera::setup_camera)
18 .add_systems(EguiPrimaryContextPass, show_ui)
19 .add_systems(Update, modify_dock_state.run_if(on_message::<UiCommand>))
20 .insert_resource(UiState::new());
21 }
22}
23
24#[derive(Resource)]
26struct UiState {
27 dock_state: DockState<AppTab>,
28 status_bar_text: String,
29}
30
31fn modify_dock_state(mut dock_state: ResMut<UiState>, mut msg_reader: MessageReader<UiCommand>) {
33 for msg in msg_reader.read() {
34 match msg {
35 UiCommand::OpenOrFocusVehicleTab(tab) => {
36 dock_state.open_or_focus_tab(tab.clone());
37 }
38 UiCommand::SetStatusBarText(text) => {
39 dock_state.status_bar_text = text.clone();
40 }
41 }
42 }
43}
44
45impl UiState {
46 fn new() -> Self {
47 Self {
48 dock_state: DockState::new(vec![AppTab::AllNames]),
49 status_bar_text: "Ready".into(),
50 }
51 }
52 fn open_or_focus_tab(&mut self, tab: AppTab) {
54 if let Some((surface_index, node_index, tab_index)) = self.dock_state.find_tab(&tab) {
55 self.dock_state
56 .set_active_tab((surface_index, node_index, tab_index));
57 self.dock_state
58 .set_focused_node_and_surface((surface_index, node_index));
59 } else {
60 self.dock_state.push_to_focused_leaf(tab);
61 }
62 }
63}
64
65#[derive(PartialEq, Clone, Copy, Debug)]
67pub enum AppTab {
68 AllNames,
69 Vehicle(Entity),
70 LineTimetable(Entity),
71}
72
73#[derive(Message)]
75pub enum UiCommand {
76 OpenOrFocusVehicleTab(AppTab),
77 SetStatusBarText(String),
78}
79
80struct AppTabViewer<'w> {
83 world: &'w mut World,
84}
85
86impl<'w> egui_dock::TabViewer for AppTabViewer<'w> {
87 type Tab = AppTab;
88
89 fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
90 match tab {
91 AppTab::AllNames => {
92 self.world
93 .run_system_cached_with(tabs::vehicle_overview::show_vehicle_overview, ui)
94 .unwrap();
95 }
96 AppTab::Vehicle(entity) => {
97 self.world
98 .run_system_cached_with(tabs::vehicle::show_vehicle, (ui, *entity))
99 .unwrap();
100 }
101 AppTab::LineTimetable(entity) => {
102 self.world
103 .run_system_cached_with(
104 tabs::line_timetable::show_line_timetable,
105 (ui, *entity),
106 )
107 .unwrap();
108 }
109 };
110 }
111
112 fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
113 match tab {
114 AppTab::AllNames => "All Names".into(),
115 AppTab::Vehicle(entity) => {
116 let name = self
118 .world
119 .get::<Name>(*entity)
120 .map_or_else(|| "Unknown Vehicle".into(), |n| format!("{}", n));
121 format!("{}", name).into()
122 }
123 AppTab::LineTimetable(_) => "Line Timetable".into(),
124 }
125 }
126
127 fn id(&mut self, tab: &mut Self::Tab) -> egui::Id {
128 match tab {
129 AppTab::Vehicle(entity) => egui::Id::new(format!("VehicleTab_{:?}", entity)),
130 AppTab::LineTimetable(entity) => {
131 egui::Id::new(format!("LineTimetableTab_{:?}", entity))
132 }
133 _ => egui::Id::new(self.title(tab).text()),
134 }
135 }
136
137 fn scroll_bars(&self, tab: &Self::Tab) -> [bool; 2] {
138 match tab {
139 AppTab::AllNames | AppTab::Vehicle(_) | AppTab::LineTimetable(_) => [false; 2],
140 }
141 }
142
143 fn on_tab_button(&mut self, tab: &mut Self::Tab, response: &egui::Response) {
144 let title = self.title(tab).text().to_string();
145 if response.hovered() {
146 self.world
147 .write_message(UiCommand::SetStatusBarText(format!("🖳 {}", title)));
148 }
149 }
150}
151
152fn show_ui(
154 world: &mut World,
155 ctx: &mut SystemState<EguiContexts>,
156 mut initialized: Local<bool>,
157 mut frame_history: Local<VecDeque<f64>>,
158 mut counter: Local<u8>,
159 mut modal_open: Local<bool>,
160) -> Result<()> {
161 let now = instant::Instant::now();
162 let mut ctx = ctx.get_mut(world);
163 let ctx = &ctx.ctx_mut().unwrap().clone();
164 if !*initialized {
165 ctx.options_mut(|options| {
166 options.max_passes = std::num::NonZeroUsize::new(3).unwrap();
167 });
168 ctx.style_mut(|style| {
169 style.spacing.window_margin = egui::Margin::same(2);
170 });
171 apply_custom_fonts(ctx);
172 *initialized = true;
173 }
174 world.resource_scope(|world, mut ui_state: Mut<UiState>| {
175 egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| {
176 world
177 .run_system_cached_with(about::show_about, (ui, &mut *modal_open))
178 .unwrap();
179 });
180
181 egui::TopBottomPanel::bottom("status_bar").show(ctx, |ui| {
182 ui.label(&ui_state.status_bar_text);
183 });
184
185 egui::SidePanel::left("TreeView").show(ctx, |ui| {
186 egui::ScrollArea::both().show(ui, |ui| {
187 world
188 .run_system_cached_with(tabs::tree_view::show_tree_view, ui)
189 .unwrap();
190 });
191 });
192
193 egui::CentralPanel::default()
194 .frame(egui::Frame::default().inner_margin(egui::Margin::same(0)))
195 .show(ctx, |ui| {
196 let painter = ui.painter();
197 let rect = ui.max_rect();
198
199 let spacing = 24.0; let stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(60, 60, 78));
201
202 let start_x = (rect.left() / spacing).floor() * spacing;
204 let end_x = rect.right();
205 let mut x = start_x;
206 while x <= end_x {
207 painter.line_segment(
208 [egui::pos2(x, rect.top()), egui::pos2(x, rect.bottom())],
209 stroke,
210 );
211 x += spacing;
212 }
213
214 let start_y = (rect.top() / spacing).floor() * spacing;
216 let end_y = rect.bottom();
217 let mut y = start_y;
218 while y <= end_y {
219 painter.line_segment(
220 [egui::pos2(rect.left(), y), egui::pos2(rect.right(), y)],
221 stroke,
222 );
223 y += spacing;
224 }
225 let mut tab_viewer = AppTabViewer { world: world };
226 DockArea::new(&mut ui_state.dock_state).show_inside(ui, &mut tab_viewer);
227 });
228 });
229 *counter = counter.wrapping_add(1);
230 frame_history.push_back(now.elapsed().as_secs_f64());
232 if frame_history.len() > 256 {
233 frame_history.pop_front();
234 }
235 if *counter == 0 {
236 debug!(
237 "UI frame took {:?} on average. Min {:?}, Max {:?}",
238 {
239 let total: f64 = frame_history.iter().sum();
240 instant::Duration::from_secs_f64(total / (frame_history.len() as f64))
241 },
242 {
243 let min = frame_history.iter().cloned().fold(f64::INFINITY, f64::min);
244 instant::Duration::from_secs_f64(min)
245 },
246 {
247 let max = frame_history
248 .iter()
249 .cloned()
250 .fold(f64::NEG_INFINITY, f64::max);
251 instant::Duration::from_secs_f64(max)
252 }
253 );
254 }
255 Ok(())
256}
257
258fn apply_custom_fonts(ctx: &egui::Context) {
260 let mut fonts = egui::FontDefinitions::default();
261
262 fonts.font_data.insert(
263 "app_default".to_owned(),
264 Arc::new(egui::FontData::from_static(include_bytes!(
265 "../assets/fonts/SarasaUiSC-Regular.ttf"
266 ))),
267 );
268
269 fonts.font_data.insert(
270 "app_mono".to_owned(),
271 Arc::new(egui::FontData::from_static(include_bytes!(
272 "../assets/fonts/SarasaTermSC-Regular.ttf"
273 ))),
274 );
275
276 if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Proportional) {
277 family.insert(0, "app_default".to_owned());
278 }
279 if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Monospace) {
280 family.insert(0, "app_mono".to_owned());
281 }
282
283 ctx.set_fonts(fonts);
284}