Skip to main content

bevy_yoleck/
editor_panels.rs

1use bevy::ecs::system::SystemId;
2use bevy::prelude::*;
3use bevy_egui::egui;
4use std::ops::{Deref, DerefMut};
5
6use crate::util::EditSpecificResources;
7
8/// An handle for the egui UI frame used in panel sections definitions
9#[derive(Resource)]
10pub struct YoleckPanelUi(pub egui::Ui);
11
12impl Deref for YoleckPanelUi {
13    type Target = egui::Ui;
14
15    fn deref(&self) -> &Self::Target {
16        &self.0
17    }
18}
19
20impl DerefMut for YoleckPanelUi {
21    fn deref_mut(&mut self) -> &mut Self::Target {
22        &mut self.0
23    }
24}
25
26pub(crate) trait EditorPanel: Resource + Sized {
27    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>>;
28    fn wrapper(
29        &mut self,
30        ctx: &mut egui::Context,
31        viewport_ui: &mut egui::Ui,
32        add_content: impl FnOnce(&mut Self, &mut egui::Ui),
33    ) -> egui::Response;
34
35    fn show_panel(
36        world: &mut World,
37        ctx: &mut egui::Context,
38        viewport_ui: &mut egui::Ui,
39    ) -> egui::Response {
40        world.resource_scope(|world, mut this: Mut<Self>| {
41            this.wrapper(ctx, viewport_ui, |this, ui| {
42                let frame = egui::Frame::new();
43                let mut prepared = frame.begin(ui);
44                let content_ui = std::mem::replace(
45                    &mut prepared.content_ui,
46                    ui.new_child(egui::UiBuilder {
47                        max_rect: Some(ui.max_rect()),
48                        layout: Some(*ui.layout()), // Is this necessary?
49                        ..Default::default()
50                    }),
51                );
52                world.insert_resource(YoleckPanelUi(content_ui));
53
54                world.resource_scope(|world, mut edit_specific: Mut<EditSpecificResources>| {
55                    edit_specific.inject_to_world(world);
56                    for section in this.iter_sections() {
57                        world.run_system(section).unwrap().unwrap();
58                    }
59                    edit_specific.take_from_world(world);
60                });
61
62                let YoleckPanelUi(content_ui) = world.remove_resource().expect(
63                    "The YoleckPanelUi resource was put in the world by this very function",
64                );
65                prepared.content_ui = content_ui;
66                prepared.end(ui);
67            })
68        })
69    }
70}
71
72/// Sections for the left panel of the Yoleck editor window.
73///
74/// Already contains sections by default, but can be used to customize the editor by adding more
75/// sections. Each section is a Bevy system in the form of a [`SystemId`] with no input and a Bevy
76/// [`Result<()>`] for an output. These can be obtained by registering a system function on the
77/// Bevy app using [`register_system`](App::register_system).
78///
79/// The section system can draw on the panel using [`YoleckPanelUi`], accessible as a [`ResMut`].
80///
81/// ```no_run
82/// # use bevy::prelude::*;
83/// # use bevy_yoleck::{YoleckEditorLeftPanelSections, egui, YoleckPanelUi};
84/// # let mut app = App::new();
85/// let time_since_startup_section = app.register_system(|mut ui: ResMut<YoleckPanelUi>, time: Res<Time>| {
86///     ui.label(format!("Time since startup is {:?}", time.elapsed()));
87///     Ok(())
88/// });
89/// app.world_mut().resource_mut::<YoleckEditorLeftPanelSections>().0.push(time_since_startup_section);
90/// ```
91#[derive(Resource)]
92pub struct YoleckEditorLeftPanelSections(pub Vec<SystemId<(), Result>>);
93
94impl FromWorld for YoleckEditorLeftPanelSections {
95    fn from_world(world: &mut World) -> Self {
96        Self(vec![
97            world.register_system(crate::editor::new_entity_section),
98            world.register_system(crate::editor::entity_selection_section),
99        ])
100    }
101}
102
103impl EditorPanel for YoleckEditorLeftPanelSections {
104    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {
105        self.0.iter().copied()
106    }
107
108    fn wrapper(
109        &mut self,
110        ctx: &mut egui::Context,
111        viewport_ui: &mut egui::Ui,
112        add_content: impl FnOnce(&mut Self, &mut egui::Ui),
113    ) -> egui::Response {
114        egui::Panel::left("yoleck_left_panel")
115            .resizable(true)
116            .default_size(300.0)
117            .max_size(ctx.content_rect().width() / 4.0)
118            .show_inside(viewport_ui, |ui| {
119                ui.heading("Level Hierarchy");
120                ui.separator();
121                egui::ScrollArea::vertical().show(ui, |ui| {
122                    add_content(self, ui);
123                });
124            })
125            .response
126    }
127}
128
129/// Sections for the right panel of the Yoleck editor window. Works the same as
130/// [`YoleckEditorLeftPanelSections`].
131#[derive(Resource)]
132pub struct YoleckEditorRightPanelSections(pub Vec<SystemId<(), Result>>);
133
134impl FromWorld for YoleckEditorRightPanelSections {
135    fn from_world(world: &mut World) -> Self {
136        Self(vec![
137            world.register_system(crate::editor::entity_editing_section),
138        ])
139    }
140}
141
142impl EditorPanel for YoleckEditorRightPanelSections {
143    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {
144        self.0.iter().copied()
145    }
146
147    fn wrapper(
148        &mut self,
149        ctx: &mut egui::Context,
150        viewport_ui: &mut egui::Ui,
151        add_content: impl FnOnce(&mut Self, &mut egui::Ui),
152    ) -> egui::Response {
153        egui::Panel::right("yoleck_right_panel")
154            .resizable(true)
155            .default_size(300.0)
156            .max_size(ctx.content_rect().width() / 4.0)
157            .show_inside(viewport_ui, |ui| {
158                ui.heading("Properties");
159                ui.separator();
160                egui::ScrollArea::vertical().show(ui, |ui| {
161                    add_content(self, ui);
162                });
163            })
164            .response
165    }
166}
167
168/// Sections for the top panel of the Yoleck editor window. Works the same as
169/// [`YoleckEditorLeftPanelSections`].
170#[derive(Resource)]
171pub struct YoleckEditorTopPanelSections(pub Vec<SystemId<(), Result>>);
172
173impl FromWorld for YoleckEditorTopPanelSections {
174    fn from_world(world: &mut World) -> Self {
175        Self(vec![
176            world.register_system(crate::level_files_manager::level_files_manager_top_section),
177            world.register_system(crate::level_files_manager::playtest_buttons_section),
178        ])
179    }
180}
181
182impl EditorPanel for YoleckEditorTopPanelSections {
183    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {
184        self.0.iter().copied()
185    }
186
187    fn wrapper(
188        &mut self,
189        _ctx: &mut egui::Context,
190        viewport_ui: &mut egui::Ui,
191        add_content: impl FnOnce(&mut Self, &mut egui::Ui),
192    ) -> egui::Response {
193        egui::Panel::top("yoleck_top_panel")
194            .resizable(false)
195            .show_inside(viewport_ui, |ui| {
196                let inner_margin = 3.;
197
198                ui.add_space(inner_margin);
199                ui.horizontal(|ui| {
200                    ui.add_space(inner_margin);
201                    ui.label("Yoleck Editor");
202                    ui.separator();
203                    add_content(self, ui);
204                    ui.add_space(inner_margin);
205                });
206                ui.add_space(inner_margin);
207            })
208            .response
209    }
210}
211
212/// A tab in the bottom panel of the Yoleck editor window.
213///
214/// The [`sections`](Self::sections) parameter is a list of [`SystemId`] obtained similarly to the
215/// ones in [`YoleckEditorLeftPanelSections`].
216pub struct YoleckEditorBottomPanelTab {
217    pub name: String,
218    pub sections: Vec<SystemId<(), Result>>,
219}
220
221/// Tabs for the bottom panel of the Yoleck editor window.
222///
223/// Works similar to [`YoleckEditorLeftPanelSections`], except instead of a single list of systems
224/// they reside within [`tabs`](Self::tabs).
225#[derive(Resource)]
226pub struct YoleckEditorBottomPanelSections {
227    pub tabs: Vec<YoleckEditorBottomPanelTab>,
228    active_tab: usize,
229}
230
231impl FromWorld for YoleckEditorBottomPanelSections {
232    fn from_world(world: &mut World) -> Self {
233        Self {
234            tabs: vec![YoleckEditorBottomPanelTab {
235                name: "Console".to_owned(),
236                sections: vec![world.register_system(crate::console::console_panel_section)],
237            }],
238            active_tab: 0,
239        }
240    }
241}
242
243impl EditorPanel for YoleckEditorBottomPanelSections {
244    fn iter_sections(&self) -> impl Iterator<Item = SystemId<(), Result>> {
245        self.tabs
246            .get(self.active_tab)
247            .into_iter()
248            .flat_map(|tab| tab.sections.iter().copied())
249    }
250
251    fn wrapper(
252        &mut self,
253        ctx: &mut egui::Context,
254        viewport_ui: &mut egui::Ui,
255        add_content: impl FnOnce(&mut Self, &mut egui::Ui),
256    ) -> egui::Response {
257        egui::Panel::bottom("yoleck_bottom_panel")
258            .resizable(true)
259            .default_size(200.0)
260            .max_size(ctx.content_rect().height() / 4.0)
261            .show_inside(viewport_ui, |ui| {
262                let inner_margin = 3.;
263                ui.add_space(inner_margin);
264
265                let mut new_active_tab = self.active_tab;
266                ui.horizontal(|ui| {
267                    for (i, tab) in self.tabs.iter().enumerate() {
268                        if ui
269                            .selectable_label(new_active_tab == i, &tab.name)
270                            .clicked()
271                        {
272                            new_active_tab = i;
273                        }
274                    }
275                });
276                self.active_tab = new_active_tab;
277
278                ui.separator();
279
280                add_content(self, ui);
281            })
282            .response
283    }
284}