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