1mod about;
2mod side_panel;
3mod tabs;
4mod widgets;
5use crate::colors;
6use crate::settings::ApplicationSettings;
7use bevy::color::palettes::tailwind::{EMERALD_700, EMERALD_800, GRAY_900};
8use bevy::prelude::*;
9use egui::{
10 self, Color32, CornerRadius, Frame, Id, Margin, Rect, Sense, Shape, Stroke, Ui, UiBuilder,
11};
12use egui_dock::{DockArea, DockState, TabInteractionStyle};
13use egui_i18n::tr;
14use moonshine_core::save::prelude::*;
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17use strum::{EnumCount, IntoEnumIterator};
18use tabs::diagram::SelectedEntityType;
19use tabs::minesweeper::MinesweeperData;
20use tabs::{Tab, all_tabs::*};
21#[cfg(target_arch = "wasm32")]
22use wasm_bindgen::prelude::wasm_bindgen;
23
24pub struct InterfacePlugin;
26
27impl Plugin for InterfacePlugin {
28 fn build(&self, app: &mut App) {
29 app.add_message::<UiCommand>()
30 .init_resource::<MiscUiState>()
31 .init_resource::<SelectedElement>()
32 .init_resource::<MinesweeperData>()
33 .init_resource::<SidePanelState>()
34 .insert_resource(UiState::new())
35 .insert_resource(StatusBarState::default())
36 .add_systems(Update, (modify_dock_state.run_if(on_message::<UiCommand>),));
37 }
38}
39
40#[derive(Resource, Clone, Reflect, Serialize, Deserialize)]
42#[reflect(Resource, MapEntities, Serialize, Deserialize, opaque)]
43pub struct UiState {
44 dock_state: DockState<AppTab>,
45}
46
47impl MapEntities for UiState {
48 fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
49 for (_, tab) in self.dock_state.iter_all_tabs_mut() {
50 tab.map_entities(entity_mapper);
51 }
52 }
53}
54
55#[derive(Default, Resource, Deref, DerefMut)]
57pub struct SelectedElement(pub Option<SelectedEntityType>);
58
59#[derive(Resource)]
61pub struct MiscUiState {
62 is_dark_mode: bool,
63 initialized: bool,
64 frame_times: egui::util::History<f32>,
65 modal_open: bool,
66 fullscreened: bool,
67 supplementary_panel_state: SupplementaryPanelState,
68}
69
70#[derive(Default)]
71pub struct SupplementaryPanelState {
72 expanded: bool,
73 is_on_bottom: bool,
74}
75
76impl Default for MiscUiState {
77 fn default() -> Self {
78 let max_age: f32 = 1.0;
79 let max_len = (max_age * 300.0).round() as usize;
80 Self {
81 is_dark_mode: true,
82 frame_times: egui::util::History::new(0..max_len, max_age),
83 initialized: false,
84 modal_open: false,
85 fullscreened: false,
86 supplementary_panel_state: SupplementaryPanelState::default(),
87 }
88 }
89}
90
91impl MiscUiState {
92 pub fn on_new_frame(&mut self, now: f64, previous_frame_time: Option<f32>) {
93 let previous_frame_time = previous_frame_time.unwrap_or_default();
94 if let Some(latest) = self.frame_times.latest_mut() {
95 *latest = previous_frame_time; }
97 self.frame_times.add(now, previous_frame_time); }
99 pub fn mean_frame_time(&self) -> f32 {
100 self.frame_times.average().unwrap_or_default()
101 }
102}
103
104#[derive(Default, Resource)]
105struct StatusBarState {
106 tooltip: String,
107}
108
109fn modify_dock_state(mut dock_state: ResMut<UiState>, mut msg_reader: MessageReader<UiCommand>) {
111 for msg in msg_reader.read() {
112 match msg {
113 UiCommand::OpenOrFocusTab(tab) => {
114 dock_state.open_or_focus_tab(tab.clone());
115 }
116 }
117 }
118}
119
120impl UiState {
121 fn new() -> Self {
122 Self {
123 dock_state: DockState::new(vec![AppTab::Start(StartTab::default())]),
124 }
125 }
126 fn open_or_focus_tab(&mut self, tab: AppTab) {
128 if let Some((surface_index, node_index, tab_index)) = self.dock_state.find_tab(&tab) {
129 self.dock_state
130 .set_active_tab((surface_index, node_index, tab_index));
131 self.dock_state
132 .set_focused_node_and_surface((surface_index, node_index));
133 } else {
134 self.dock_state.push_to_focused_leaf(tab);
135 }
136 }
137}
138
139macro_rules! for_all_tabs {
140 ($tab:expr, $t:ident, $body:expr) => {
141 match $tab {
142 AppTab::Start($t) => $body,
143 AppTab::Vehicle($t) => $body,
144 AppTab::StationTimetable($t) => $body,
145 AppTab::Diagram($t) => $body,
146 AppTab::DisplayedLines($t) => $body,
147 AppTab::Settings($t) => $body,
148 AppTab::Classes($t) => $body,
149 AppTab::Services($t) => $body,
150 AppTab::Minesweeper($t) => $body,
151 AppTab::Graph($t) => $body,
152 AppTab::Inspector($t) => $body,
153 }
154 };
155}
156
157#[derive(Clone, Serialize, Deserialize)]
159pub enum AppTab {
160 Start(StartTab),
161 Vehicle(VehicleTab),
162 StationTimetable(StationTimetableTab),
163 Diagram(DiagramTab),
164 DisplayedLines(DisplayedLinesTab),
165 Settings(SettingsTab),
166 Classes(ClassesTab),
167 Services(ServicesTab),
168 Minesweeper(MinesweeperTab),
169 Graph(GraphTab),
170 Inspector(InspectorTab),
171}
172
173impl MapEntities for AppTab {
174 fn map_entities<M: EntityMapper>(&mut self, entity_mapper: &mut M) {
175 match self {
176 AppTab::Vehicle(tab) => tab.map_entities(entity_mapper),
177 AppTab::StationTimetable(tab) => tab.map_entities(entity_mapper),
178 AppTab::Diagram(tab) => tab.map_entities(entity_mapper),
179 AppTab::Graph(tab) => tab.map_entities(entity_mapper),
180 AppTab::Start(_)
181 | AppTab::DisplayedLines(_)
182 | AppTab::Settings(_)
183 | AppTab::Classes(_)
184 | AppTab::Services(_)
185 | AppTab::Inspector(_)
186 | AppTab::Minesweeper(_) => {}
187 }
188 }
189}
190
191impl AppTab {
192 pub fn id(&self) -> egui::Id {
193 for_all_tabs!(self, t, t.id())
194 }
195 pub fn color(&self) -> Color32 {
196 let num = self.id().value();
197 let color = colors::PredefinedColor::iter()
199 .nth(num as usize % colors::PredefinedColor::COUNT)
200 .unwrap();
201 color.get(true)
202 }
203}
204
205impl PartialEq for AppTab {
206 fn eq(&self, other: &Self) -> bool {
207 self.id() == other.id()
208 }
209}
210
211#[derive(Message)]
213pub enum UiCommand {
214 OpenOrFocusTab(AppTab),
215}
216
217struct AppTabViewer<'w> {
220 world: &'w mut World,
221 ctx: &'w egui::Context,
222 focused_id: Option<egui::Id>,
223}
224
225impl<'w> egui_dock::TabViewer for AppTabViewer<'w> {
226 type Tab = AppTab;
227
228 fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
229 for_all_tabs!(
230 tab,
231 t,
232 t.frame().show(ui, |ui| t.main_display(self.world, ui))
233 );
234
235 let is_focused = self.focused_id == Some(tab.id());
237 let strength = ui
238 .ctx()
239 .animate_bool(ui.id().with("focus_highlight"), is_focused);
240 if strength > 0.0 {
241 ui.painter().rect_stroke(
242 ui.clip_rect(),
243 0,
244 Stroke {
245 width: 1.8,
246 color: tab.color().linear_multiply(strength),
247 },
248 egui::StrokeKind::Inside,
249 );
250 }
251 }
252
253 fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
254 for_all_tabs!(tab, t, t.title())
255 }
256
257 fn id(&mut self, tab: &mut Self::Tab) -> egui::Id {
258 tab.id()
259 }
260
261 fn scroll_bars(&self, tab: &Self::Tab) -> [bool; 2] {
262 for_all_tabs!(tab, t, t.scroll_bars())
263 }
264
265 fn on_tab_button(&mut self, tab: &mut Self::Tab, response: &egui::Response) {
266 for_all_tabs!(tab, t, t.on_tab_button(self.world, response))
267 }
268
269 fn tab_style_override(
270 &self,
271 tab: &Self::Tab,
272 global_style: &egui_dock::TabStyle,
273 ) -> Option<egui_dock::TabStyle> {
274 Some(egui_dock::TabStyle {
275 focused: TabInteractionStyle {
276 bg_fill: tab.color().gamma_multiply_u8(180),
277 text_color: if self.ctx.theme().default_visuals().dark_mode {
278 Color32::WHITE
279 } else {
280 Color32::BLACK
281 },
282 ..global_style.focused
283 },
284 active: TabInteractionStyle {
285 bg_fill: tab.color().gamma_multiply_u8(120),
286 ..global_style.active
287 },
288 active_with_kb_focus: TabInteractionStyle {
289 bg_fill: tab.color().gamma_multiply_u8(120),
290 ..global_style.active_with_kb_focus
291 },
292 hovered: TabInteractionStyle {
293 bg_fill: tab.color().gamma_multiply_u8(120),
294 ..global_style.hovered
295 },
296 inactive: TabInteractionStyle {
297 bg_fill: tab.color().gamma_multiply_u8(90),
298 ..global_style.inactive
299 },
300 inactive_with_kb_focus: TabInteractionStyle {
301 bg_fill: tab.color().gamma_multiply_u8(90),
302 ..global_style.inactive_with_kb_focus
303 },
304 ..global_style.clone()
305 })
306 }
307}
308
309#[derive(Resource)]
310struct SidePanelState {
311 dock_state: DockState<SidePanelTab>,
312}
313
314impl Default for SidePanelState {
315 fn default() -> Self {
316 let dock_state = DockState::new(vec![
317 SidePanelTab::Edit,
318 SidePanelTab::Details,
319 SidePanelTab::Export,
320 ]);
321 Self { dock_state }
322 }
323}
324
325enum SidePanelTab {
326 Edit,
327 Details,
328 Export,
329}
330
331struct SidePanelViewer<'w> {
332 world: &'w mut World,
333 focused_tab: Option<&'w mut AppTab>,
334}
335
336impl<'w> egui_dock::TabViewer for SidePanelViewer<'w> {
337 type Tab = SidePanelTab;
338 fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
339 let Some(focused_tab) = self.focused_tab.as_deref_mut() else {
340 ui.label("No tabs focused. Open a tab to see its properties.");
341 return;
342 };
343 use egui::NumExt;
344 let dt = ui.ctx().input(|input| input.stable_dt).at_most(0.1);
345 let t = egui::emath::exponential_smooth_factor(0.9, 0.2, dt);
346 let mut repaint = false;
347 let opacity = ui.ctx().data_mut(|map| {
348 let opacity: &mut f32 = map.get_temp_mut_or(Id::new("side panel opacity"), 0.0);
349 if *opacity > 0.99 {
350 *opacity = 1.0;
351 } else {
352 *opacity = emath::lerp(*opacity..=1.0, t);
353 repaint = true;
354 }
355 *opacity
356 });
357 if repaint {
358 ui.ctx().request_repaint();
359 }
360 ui.multiply_opacity(opacity);
361 match tab {
362 SidePanelTab::Edit => {
363 for_all_tabs!(focused_tab, t, {
364 egui::Frame::new()
365 .inner_margin(4)
366 .show(ui, |ui| t.edit_display(self.world, ui));
367 })
368 }
369 SidePanelTab::Details => {
370 for_all_tabs!(focused_tab, t, {
371 egui::Frame::new()
372 .inner_margin(4)
373 .show(ui, |ui| t.display_display(self.world, ui));
374 })
375 }
376 SidePanelTab::Export => {
377 for_all_tabs!(focused_tab, t, {
378 egui::Frame::new()
379 .inner_margin(4)
380 .show(ui, |ui| t.export_display(self.world, ui));
381 })
382 }
383 }
384 }
385 fn on_tab_button(&mut self, _tab: &mut Self::Tab, response: &egui::Response) {
386 if !response.clicked() {
387 return;
388 }
389 let reload = response.ctx.data_mut(|map| {
391 let previous_tab =
393 map.get_temp_mut_or(Id::new("side panel previous tab id"), response.id);
394 if *previous_tab != response.id {
395 *previous_tab = response.id;
396 true
397 } else {
398 false
399 }
400 });
401 if !reload {
402 return;
403 }
404 response.ctx.data_mut(|map| {
406 let opacity: &mut f32 = map.get_temp_mut_or(Id::new("side panel opacity"), 0.0);
407 *opacity = 0.0;
408 });
409 }
410 fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
411 match tab {
412 SidePanelTab::Edit => tr!("side-panel-edit"),
413 SidePanelTab::Details => tr!("side-panel-details"),
414 SidePanelTab::Export => tr!("side-panel-export"),
415 }
416 .into()
417 }
418 fn id(&mut self, tab: &mut Self::Tab) -> Id {
419 match tab {
420 SidePanelTab::Edit => Id::new("edit"),
421 SidePanelTab::Details => Id::new("details"),
422 SidePanelTab::Export => Id::new("export"),
423 }
424 }
425 fn is_closeable(&self, _tab: &Self::Tab) -> bool {
426 false
427 }
428}
429
430#[cfg(target_arch = "wasm32")]
433#[wasm_bindgen(inline_js = r#"
434export function go_fullscreen(id) {
435 const el = document.getElementById(id);
436 if (el?.requestFullscreen) el.requestFullscreen();
437}
438export function exit_fullscreen() {
439 if (document.fullscreenElement) {
440 document.exitFullscreen();
441 }
442}
443"#)]
444unsafe extern "C" {
445 fn go_fullscreen(id: &str);
446 fn exit_fullscreen();
447}
448
449pub fn show_ui(app: &mut super::PaiagramApp, ctx: &egui::Context) -> Result<()> {
451 ctx.request_repaint_after(std::time::Duration::from_millis(500));
452 let mut mus = app
453 .bevy_app
454 .world_mut()
455 .remove_resource::<MiscUiState>()
456 .unwrap();
457 let mut side_panel_state = app
458 .bevy_app
459 .world_mut()
460 .remove_resource::<SidePanelState>()
461 .unwrap();
462 let frame_time = mus.mean_frame_time();
463 app.bevy_app
464 .world_mut()
465 .resource_scope(|world, mut ui_state: Mut<UiState>| {
466 for (_, tab) in ui_state.dock_state.iter_all_tabs_mut() {
467 for_all_tabs!(tab, t, t.pre_render(world));
468 }
469 egui::TopBottomPanel::top("menu_bar")
470 .frame(Frame::side_top_panel(&ctx.style()))
471 .show(&ctx, |ui| {
472 ui.horizontal(|ui| {
473 ui.checkbox(&mut mus.is_dark_mode, "D").changed().then(|| {
474 if mus.is_dark_mode {
475 ctx.set_theme(egui::Theme::Dark);
476 } else {
477 ctx.set_theme(egui::Theme::Light);
478 }
479 });
480 #[cfg(not(target_arch = "wasm32"))]
481 if ui.button("F").clicked() {
482 ui.ctx()
483 .send_viewport_cmd(egui::ViewportCommand::Fullscreen(
484 !mus.fullscreened,
485 ));
486 mus.fullscreened = !mus.fullscreened;
487 }
488 #[cfg(target_arch = "wasm32")]
489 if ui.button("F").clicked() {
490 unsafe {
492 if mus.fullscreened {
493 exit_fullscreen();
494 } else {
495 go_fullscreen("paiagram_canvas");
496 }
497 }
498 mus.fullscreened = !mus.fullscreened;
499 }
500 if ui.button("S").clicked() {
501 mus.supplementary_panel_state.is_on_bottom =
502 !mus.supplementary_panel_state.is_on_bottom;
503 }
504 if ui.button("A").clicked() {
505 mus.supplementary_panel_state.expanded =
506 !mus.supplementary_panel_state.expanded;
507 }
508 world
509 .run_system_cached_with(about::show_about, (ui, &mut mus.modal_open))
510 .unwrap();
511 })
512 });
513
514 let old_bg_stroke_color = ctx.style().visuals.widgets.noninteractive.bg_stroke.color;
515
516 ctx.style_mut(|s| {
517 s.visuals.widgets.noninteractive.bg_stroke.color =
518 colors::translate_srgba_to_color32(EMERALD_700)
519 });
520 egui::TopBottomPanel::bottom("status_bar")
522 .frame(
523 Frame::side_top_panel(&ctx.style())
524 .fill(colors::translate_srgba_to_color32(EMERALD_800)),
525 )
526 .show(&ctx, |ui| {
527 ui.visuals_mut().override_text_color = Some(Color32::from_gray(200));
528 ui.horizontal(|ui| {
529 ui.label(&world.resource::<StatusBarState>().tooltip);
530 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
531 let current_time = chrono::Local::now();
532 ui.monospace(current_time.format("%H:%M:%S").to_string());
533 if !world
534 .resource::<ApplicationSettings>()
535 .show_performance_stats
536 {
537 return;
538 }
539 ui.monospace(format!("eFPS: {:>6.1}", 1.0 / frame_time));
540 ui.monospace(format!("{:>5.2} ms/f", 1e3 * frame_time));
541 });
542 });
543 });
544
545 ctx.style_mut(|s| {
546 s.visuals.widgets.noninteractive.bg_stroke.color = old_bg_stroke_color
547 });
548
549 let supplementary_panel_content = |ui: &mut Ui| {
550 let focused_tab = ui_state
551 .dock_state
552 .find_active_focused()
553 .map(|(_, tab)| tab);
554 let mut side_panel_viewer = SidePanelViewer { world, focused_tab };
555 let mut style = egui_dock::Style::from_egui(ui.style());
556 style.tab.tab_body.inner_margin = Margin::same(1);
557 style.tab.tab_body.corner_radius = CornerRadius::ZERO;
558 style.tab.tab_body.stroke.width = 0.0;
559 style.tab_bar.corner_radius = CornerRadius::ZERO;
560 DockArea::new(&mut side_panel_state.dock_state)
561 .id(Id::new("Side panel stuff"))
562 .draggable_tabs(false)
563 .show_leaf_close_all_buttons(false)
564 .show_leaf_collapse_buttons(false)
565 .style(style)
566 .show_inside(ui, &mut side_panel_viewer);
567 };
568
569 if mus.supplementary_panel_state.is_on_bottom {
570 egui::TopBottomPanel::bottom("TreeView")
571 .frame(egui::Frame::new())
572 .resizable(false)
573 .exact_height(ctx.used_size().y / 2.5)
574 .show_animated(
575 ctx,
576 mus.supplementary_panel_state.expanded,
577 supplementary_panel_content,
578 );
579 } else {
580 egui::SidePanel::left("TreeView")
581 .frame(egui::Frame::new())
582 .default_width(ctx.used_size().x / 4.0)
583 .show_animated(
584 &ctx,
585 mus.supplementary_panel_state.expanded,
586 supplementary_panel_content,
587 );
588 }
589
590 egui::CentralPanel::default()
591 .frame(egui::Frame::central_panel(&ctx.style()).inner_margin(Margin::ZERO))
592 .show(&ctx, |ui| {
593 let painter = ui.painter();
594 let max_rect = ui.max_rect();
595 painter.rect_filled(
596 max_rect,
597 CornerRadius::ZERO,
598 colors::translate_srgba_to_color32(GRAY_900),
599 );
600 const LINE_SPACING: f32 = 24.0;
601 const LINE_STROKE: Stroke = Stroke {
602 color: Color32::from_additive_luminance(30),
603 width: 1.0,
604 };
605 let x_max = (ui.available_size().x / LINE_SPACING) as usize;
606 let y_max = (ui.available_size().y / LINE_SPACING) as usize;
607 for xi in 0..=x_max {
608 let mut x = xi as f32 * LINE_SPACING + max_rect.min.x;
609 LINE_STROKE.round_center_to_pixel(ui.pixels_per_point(), &mut x);
610 painter.vline(x, max_rect.min.y..=max_rect.max.y, LINE_STROKE);
611 }
612 for yi in 0..=y_max {
613 let mut y = yi as f32 * LINE_SPACING + max_rect.min.y;
614 LINE_STROKE.round_center_to_pixel(ui.pixels_per_point(), &mut y);
615 painter.hline(max_rect.min.x..=max_rect.max.x, y, LINE_STROKE);
616 }
617 let focused_id = ui_state
618 .dock_state
619 .find_active_focused()
620 .map(|(_, tab)| tab.id());
621 ui.ctx().data_mut(|map| {
622 let previously_focused =
623 map.get_temp_mut_or(Id::new("previously focused leaf"), focused_id);
624 if *previously_focused == focused_id {
625 return;
626 } else {
627 *previously_focused = focused_id;
628 let opacity: &mut f32 =
629 map.get_temp_mut_or(Id::new("side panel opacity"), 0.0);
630 *opacity = 0.0;
631 }
632 });
633 let mut tab_viewer = AppTabViewer {
634 world,
635 ctx,
636 focused_id,
637 };
638 let mut style = egui_dock::Style::from_egui(ui.style());
639 style.tab.tab_body.inner_margin = Margin::same(0);
640 style.tab.tab_body.corner_radius = CornerRadius::ZERO;
641 style.tab.tab_body.stroke.width = 0.0;
642 style.tab.active.outline_color = Color32::TRANSPARENT;
643 style.tab.inactive.outline_color = Color32::TRANSPARENT;
644 style.tab.focused.outline_color = Color32::TRANSPARENT;
645 style.tab.hovered.outline_color = Color32::TRANSPARENT;
646 style.tab.inactive_with_kb_focus.outline_color = Color32::TRANSPARENT;
647 style.tab.active_with_kb_focus.outline_color = Color32::TRANSPARENT;
648 style.tab.focused_with_kb_focus.text_color = Color32::TRANSPARENT;
649 style.tab.active.corner_radius = CornerRadius::ZERO;
650 style.tab.inactive.corner_radius = CornerRadius::ZERO;
651 style.tab.focused.corner_radius = CornerRadius::ZERO;
652 style.tab.hovered.corner_radius = CornerRadius::ZERO;
653 style.tab.inactive_with_kb_focus.corner_radius = CornerRadius::ZERO;
654 style.tab.active_with_kb_focus.corner_radius = CornerRadius::ZERO;
655 style.tab.focused_with_kb_focus.corner_radius = CornerRadius::ZERO;
656 style.tab_bar.corner_radius = CornerRadius::ZERO;
657 style.tab_bar.hline_color = Color32::TRANSPARENT;
658 style.tab.hline_below_active_tab_name = true;
659 style.overlay.selection_corner_radius = CornerRadius::same(4);
660 style.tab_bar.height = 32.0;
661 let left_bottom = ui.max_rect().left_bottom();
663 let shift = egui::Vec2 { x: 20.0, y: -40.0 };
664 DockArea::new(&mut ui_state.dock_state)
665 .style(style)
666 .show_inside(ui, &mut tab_viewer);
667 let res = ui.place(
668 Rect::from_two_pos(left_bottom, left_bottom + shift),
669 |ui: &mut Ui| {
670 let (resp, painter) =
671 ui.allocate_painter(ui.available_size(), Sense::click());
672 let rect = resp.rect;
673 painter.add(Shape::convex_polygon(
674 vec![
675 rect.left_bottom() + egui::Vec2 { x: 0.0, y: 0.0 },
676 rect.left_bottom() + egui::Vec2 { x: 0.0, y: -40.0 },
677 rect.left_bottom() + egui::Vec2 { x: 20.0, y: -20.0 },
678 rect.left_bottom() + egui::Vec2 { x: 20.0, y: 0.0 },
679 ],
680 ui.visuals().widgets.hovered.bg_fill,
681 Stroke::NONE,
682 ));
683 resp
684 },
685 );
686 if res.clicked() {
687 mus.supplementary_panel_state.expanded =
688 !mus.supplementary_panel_state.expanded;
689 }
690 });
691 for (_, tab) in ui_state.dock_state.iter_all_tabs_mut() {
692 for_all_tabs!(tab, t, t.post_render(world));
693 }
694 });
695 app.bevy_app.world_mut().insert_resource(mus);
696 app.bevy_app.world_mut().insert_resource(side_panel_state);
697 Ok(())
698}
699
700pub fn apply_custom_fonts(ctx: &egui::Context) {
702 let mut fonts = egui::FontDefinitions::default();
703
704 fonts.font_data.insert(
705 "app_default".to_owned(),
706 Arc::new(egui::FontData::from_static(include_bytes!(
707 "../assets/fonts/SarasaUiSC-Regular.ttf"
708 ))),
709 );
710
711 fonts.font_data.insert(
712 "app_mono".to_owned(),
713 Arc::new(egui::FontData::from_static(include_bytes!(
714 "../assets/fonts/SarasaTermSC-Regular.ttf"
715 ))),
716 );
717
718 if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Proportional) {
719 family.insert(0, "app_default".to_owned());
720 }
721 if let Some(family) = fonts.families.get_mut(&egui::FontFamily::Monospace) {
722 family.insert(0, "app_mono".to_owned());
723 }
724
725 ctx.set_fonts(fonts);
726}