paiagram/
interface.rs

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
10/// Plugin that sets up the user interface
11pub 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/// The state of the user interface
25#[derive(Resource)]
26struct UiState {
27    dock_state: DockState<AppTab>,
28    status_bar_text: String,
29}
30
31/// Modify the dock state based on UI commands
32fn 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    /// Open a tab if it is not already open, or focus it if it is
53    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/// An application tab
66#[derive(PartialEq, Clone, Copy, Debug)]
67pub enum AppTab {
68    AllNames,
69    Vehicle(Entity),
70    LineTimetable(Entity),
71}
72
73/// User interface commands sent between systems
74#[derive(Message)]
75pub enum UiCommand {
76    OpenOrFocusVehicleTab(AppTab),
77    SetStatusBarText(String),
78}
79
80/// A viewer for application tabs. This struct holds a single mutable reference to the world,
81/// and is constructed each frame.
82struct 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                // query the vehicle name from the world
117                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
152/// Main function to show the user interface
153fn 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; // grid cell size
200                let stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(60, 60, 78));
201
202                // vertical lines
203                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                // horizontal lines
215                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    // keep a frame history of 256 frames
231    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
258/// Apply custom fonts to the egui context
259fn 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}