1use std::collections::HashMap;
2use std::path::{Path, PathBuf};
3
4use libdqg::ecs::Entity;
5use libdqg::glam;
6use libdqg::scripting::{ScriptAttachment, ScriptList};
7use libdqg::world::{CameraComponent, Renderable, Transform, World};
8
9use crate::editor_settings::EditorSettings;
10use crate::project::{Project, RenderableAsset, RenderableKind};
11
12const ASSET_PREVIEW_SIZE: f32 = 56.0;
14
15#[derive(Default)]
20pub struct UiRequests {
21 pub new_project: Option<PathBuf>,
22 pub open_project: Option<PathBuf>,
23 pub attach_renderable: Option<(Entity, RenderableKind, PathBuf)>,
24 pub toggle_play: bool,
29 pub export_project: Option<PathBuf>,
32 pub switch_scene: Option<PathBuf>,
37 pub close_scene_tab: Option<PathBuf>,
40 pub rename_scene: Option<(PathBuf, String)>,
44 pub rename_script: Option<(PathBuf, String)>,
49 pub set_start_scene: Option<PathBuf>,
52 pub edited: bool,
57 pub save_scene: bool,
61}
62
63pub fn draw(
68 ui: &mut egui::Ui,
69 world: &mut World,
70 selected: &mut Option<Entity>,
71 renaming: &mut Option<Entity>,
72 rename_buffer: &mut String,
73 project: Option<&Project>,
74 entity_assets: &mut HashMap<Entity, RenderableAsset>,
75 assets_expanded: &mut bool,
76 texture_previews: &mut HashMap<PathBuf, egui::TextureHandle>,
77 is_playing: bool,
78 script_errors: &[String],
79 settings: &mut EditorSettings,
80 renaming_script: &mut Option<PathBuf>,
81 script_rename_buffer: &mut String,
82 renaming_scene: &mut Option<PathBuf>,
83 scene_rename_buffer: &mut String,
84 export_status: Option<&str>,
85 current_scene: &Path,
86 current_scene_dirty: bool,
87 open_scenes: &[(PathBuf, bool)],
88 active_scene: usize,
89 requests: &mut UiRequests,
90) {
91 draw_menu_bar(ui, project, is_playing, current_scene_dirty, requests);
92 draw_scene_tabs(ui, open_scenes, active_scene, requests);
93 draw_assets_panel(
94 ui,
95 project,
96 assets_expanded,
97 texture_previews,
98 settings,
99 renaming_script,
100 script_rename_buffer,
101 renaming_scene,
102 scene_rename_buffer,
103 current_scene,
104 requests,
105 );
106 draw_hierarchy(ui, world, selected, renaming, rename_buffer, entity_assets, requests);
107 draw_inspector(ui, world, selected, project, entity_assets, is_playing, requests);
108 draw_script_error_overlay(ui, script_errors);
109 draw_export_status_overlay(ui, export_status);
110}
111
112fn draw_scene_tabs(ui: &mut egui::Ui, open_scenes: &[(PathBuf, bool)], active_scene: usize, requests: &mut UiRequests) {
121 egui::Panel::top("scene_tabs_panel").show(ui, |ui| {
122 ui.horizontal(|ui| {
123 for (index, (path, dirty)) in open_scenes.iter().enumerate() {
124 let is_active = index == active_scene;
125 let label = if *dirty { format!("{} *", asset_file_name(path)) } else { asset_file_name(path) };
126 if ui.selectable_label(is_active, label).clicked() && !is_active {
127 requests.switch_scene = Some(path.clone());
128 }
129 if open_scenes.len() > 1 && ui.small_button("x").clicked() {
130 requests.close_scene_tab = Some(path.clone());
131 }
132 ui.separator();
133 }
134 });
135 });
136}
137
138fn draw_menu_bar(ui: &mut egui::Ui, project: Option<&Project>, is_playing: bool, current_scene_dirty: bool, requests: &mut UiRequests) {
139 egui::Panel::top("menu_bar_panel").show(ui, |ui| {
140 ui.horizontal(|ui| {
141 ui.menu_button("File", |ui| {
142 if ui.button("New Project...").clicked() {
143 if let Some(root) = rfd::FileDialog::new().pick_folder() {
144 requests.new_project = Some(root);
145 }
146 ui.close();
147 }
148 if ui.button("Open Project...").clicked() {
149 if let Some(root) = rfd::FileDialog::new().pick_folder() {
150 requests.open_project = Some(root);
151 }
152 ui.close();
153 }
154 ui.add_enabled_ui(project.is_some() && !is_playing, |ui| {
159 if ui.button("Save").clicked() {
160 requests.save_scene = true;
161 ui.close();
162 }
163 if ui.button("Export...").clicked() {
164 if let Some(output_dir) = rfd::FileDialog::new().pick_folder() {
165 requests.export_project = Some(output_dir);
166 }
167 ui.close();
168 }
169 });
170 });
171
172 ui.add_enabled_ui(project.is_some(), |ui| {
175 if ui.button(if is_playing { "Stop" } else { "Play" }).clicked() {
176 requests.toggle_play = true;
177 }
178 });
179
180 if let Some(project) = project {
181 let dirty_marker = if current_scene_dirty { " *" } else { "" };
182 ui.label(format!("Project: {}{dirty_marker}", project.manifest.name));
183 }
184 });
185 });
186}
187
188fn draw_hierarchy(
189 ui: &mut egui::Ui,
190 world: &mut World,
191 selected: &mut Option<Entity>,
192 renaming: &mut Option<Entity>,
193 rename_buffer: &mut String,
194 entity_assets: &mut HashMap<Entity, RenderableAsset>,
195 requests: &mut UiRequests
196) {
197 egui::Panel::left("hierarchy_panel").show(ui, |ui| {
198 ui.heading("Hierarchy");
199 ui.separator();
200
201 ui.menu_button("+ Add Entity", |ui| {
202 if ui.button("Empty").clicked() {
203 let name = format!("Entity {}", world.iter_entities().count() + 1);
204 let entity = world.spawn_empty(name, Transform::default());
205 *selected = Some(entity);
206 *renaming = None;
207 requests.edited = true;
208 ui.close();
209 }
210 if ui.button("Camera").clicked() {
211 let name = format!("Camera {}", world.iter_entities().count() + 1);
212 let entity = world.spawn_empty(name, Transform::default());
213 world.set_camera(entity, CameraComponent::default());
214 *selected = Some(entity);
215 *renaming = None;
216 requests.edited = true;
217 ui.close();
218 }
219 });
220 ui.separator();
221
222 let entities: Vec<Entity> = world.iter_entities().collect();
223 let mut despawn_requested: Option<Entity> = None;
224 for entity in entities {
225 let label = world
226 .names
227 .get(entity)
228 .map(|name| name.0.clone())
229 .unwrap_or_else(|| "<unnamed>".to_string());
230
231 if *renaming == Some(entity) {
232 let response = ui.text_edit_singleline(rename_buffer);
233 if response.lost_focus() {
234 if let Some(name) = world.names.get_mut(entity) {
235 name.0 = rename_buffer.clone();
236 }
237 *renaming = None;
238 requests.edited = true;
239 } else {
240 response.request_focus();
241 }
242 continue;
243 }
244
245 let is_selected = *selected == Some(entity);
246 let response = ui.selectable_label(is_selected, label.clone());
247 if response.clicked() {
248 *selected = Some(entity);
249 }
250 if response.double_clicked() {
251 *renaming = Some(entity);
252 *rename_buffer = label.clone();
253 }
254 response.context_menu(|ui| {
257 if ui.button("Rename").clicked() {
258 *selected = Some(entity);
259 *renaming = Some(entity);
260 *rename_buffer = label.clone();
261 ui.close();
262 }
263 if ui.button("Duplicate").clicked() {
264 if let Some(name) = world.names.get(entity) {
265 let new_name = format!("{} (copy)", name.0);
266 let new_entity = world.spawn_empty(new_name, *world.transforms.get(entity).unwrap_or(&Transform::default()));
267 if let Some(asset) = entity_assets.get(&entity) {
268 requests.attach_renderable = Some((new_entity, asset.kind(), asset.path().to_path_buf()));
269 }
270 if let Some(component) = world.cameras.get(entity).copied() {
271 world.set_camera(new_entity, component);
272 }
273 requests.edited = true;
274 }
275 ui.close();
276 }
277 if ui.button("Delete").clicked() {
278 despawn_requested = Some(entity);
279 ui.close();
280 }
281 });
282 }
283
284 if let Some(entity) = despawn_requested {
285 world.despawn(entity);
286 entity_assets.remove(&entity);
287 requests.edited = true;
288 if *selected == Some(entity) {
289 *selected = None;
290 }
291 if *renaming == Some(entity) {
292 *renaming = None;
293 }
294 }
295 });
296}
297
298fn draw_assets_panel(
299 ui: &mut egui::Ui,
300 project: Option<&Project>,
301 assets_expanded: &mut bool,
302 texture_previews: &mut HashMap<PathBuf, egui::TextureHandle>,
303 settings: &mut EditorSettings,
304 renaming_script: &mut Option<PathBuf>,
305 script_rename_buffer: &mut String,
306 renaming_scene: &mut Option<PathBuf>,
307 scene_rename_buffer: &mut String,
308 current_scene: &Path,
309 requests: &mut UiRequests,
310) {
311 egui::Panel::bottom("assets_panel").show(ui, |ui| {
312 ui.horizontal(|ui| {
313 let toggle_icon = if *assets_expanded { "v" } else { ">" };
316 if ui.small_button(toggle_icon).clicked() {
317 *assets_expanded = !*assets_expanded;
318 }
319 ui.heading("Assets");
320 });
321
322 if !*assets_expanded {
323 return;
324 }
325 ui.separator();
326
327 let Some(project) = project else {
328 ui.label("Open or create a project to import assets.");
329 return;
330 };
331
332 egui::ScrollArea::horizontal().show(ui, |ui| {
333 ui.horizontal(|ui| {
334 draw_texture_group(ui, project, texture_previews);
335 ui.separator();
336 draw_model_group(ui, project);
337 ui.separator();
338 draw_script_group(ui, project, settings, renaming_script, script_rename_buffer, requests);
339 ui.separator();
340 draw_scene_group(ui, project, current_scene, renaming_scene, scene_rename_buffer, requests);
341 });
342 });
343 });
344}
345
346fn draw_texture_group(ui: &mut egui::Ui, project: &Project, previews: &mut HashMap<PathBuf, egui::TextureHandle>) {
347 ui.vertical(|ui| {
348 draw_group_header(ui, "Textures", &["png", "jpg", "jpeg"], project);
349 ui.horizontal_wrapped(|ui| {
350 for path in project.list_assets(RenderableKind::Sprite) {
351 draw_texture_tile(ui, project, &path, previews);
352 }
353 });
354 });
355}
356
357fn draw_model_group(ui: &mut egui::Ui, project: &Project) {
358 ui.vertical(|ui| {
359 draw_group_header(ui, "Models", &["obj"], project);
360 ui.horizontal_wrapped(|ui| {
361 for path in project.list_assets(RenderableKind::Model) {
362 draw_model_tile(ui, &path);
363 }
364 });
365 });
366}
367
368fn draw_script_group(
369 ui: &mut egui::Ui,
370 project: &Project,
371 settings: &mut EditorSettings,
372 renaming_script: &mut Option<PathBuf>,
373 script_rename_buffer: &mut String,
374 requests: &mut UiRequests,
375) {
376 ui.vertical(|ui| {
377 ui.horizontal(|ui| {
378 draw_group_header(ui, "Scripts", &["rhai"], project);
379 if ui.small_button("New Script").clicked() {
382 if let Err(e) = project.create_script() {
383 eprintln!("Failed to create script: {e}");
384 }
385 }
386 });
387 ui.horizontal_wrapped(|ui| {
388 for path in project.list_scripts() {
389 draw_script_tile(ui, project, &path, settings, renaming_script, script_rename_buffer, requests);
390 }
391 });
392 });
393}
394
395fn draw_scene_group(
400 ui: &mut egui::Ui,
401 project: &Project,
402 current_scene: &Path,
403 renaming_scene: &mut Option<PathBuf>,
404 scene_rename_buffer: &mut String,
405 requests: &mut UiRequests,
406) {
407 ui.vertical(|ui| {
408 ui.horizontal(|ui| {
409 ui.label("Scenes");
410 if ui.small_button("New Scene").clicked() {
411 if let Err(e) = project.create_scene() {
412 eprintln!("Failed to create scene: {e}");
413 }
414 }
415 });
416 ui.horizontal_wrapped(|ui| {
417 for path in project.list_scenes() {
418 draw_scene_tile(ui, &path, current_scene, &project.manifest.start_scene, renaming_scene, scene_rename_buffer, requests);
419 }
420 });
421 });
422}
423
424fn draw_scene_tile(
435 ui: &mut egui::Ui,
436 path: &Path,
437 current_scene: &Path,
438 start_scene: &Path,
439 renaming_scene: &mut Option<PathBuf>,
440 scene_rename_buffer: &mut String,
441 requests: &mut UiRequests,
442) {
443 ui.vertical(|ui| {
444 ui.set_width(ASSET_PREVIEW_SIZE);
445
446 if renaming_scene.as_deref() == Some(path) {
447 let response = ui.text_edit_singleline(scene_rename_buffer);
448 if response.lost_focus() {
449 requests.rename_scene = Some((path.to_path_buf(), scene_rename_buffer.clone()));
450 *renaming_scene = None;
451 } else {
452 response.request_focus();
453 }
454 return;
455 }
456
457 let (rect, response) =
458 ui.allocate_exact_size(egui::vec2(ASSET_PREVIEW_SIZE, ASSET_PREVIEW_SIZE), egui::Sense::click());
459 let is_open = path == current_scene;
460 let fill = if is_open { egui::Color32::from_gray(80) } else { egui::Color32::from_gray(55) };
461 ui.painter().rect_filled(rect, 4.0, fill);
462 ui.painter().text(
463 rect.center(),
464 egui::Align2::CENTER_CENTER,
465 "RON",
466 egui::FontId::proportional(12.0),
467 egui::Color32::from_gray(200),
468 );
469 if path == start_scene {
470 ui.painter().text(
471 rect.right_top(),
472 egui::Align2::RIGHT_TOP,
473 "*",
474 egui::FontId::proportional(14.0),
475 egui::Color32::YELLOW,
476 );
477 }
478
479 if response.double_clicked() {
480 requests.switch_scene = Some(path.to_path_buf());
481 }
482 response.context_menu(|ui| {
483 if ui.button("Rename").clicked() {
484 *renaming_scene = Some(path.to_path_buf());
485 *scene_rename_buffer = path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
486 ui.close();
487 }
488 if ui.button("Set as Start Scene").clicked() {
489 requests.set_start_scene = Some(path.to_path_buf());
490 ui.close();
491 }
492 });
493
494 ui.add(egui::Label::new(asset_file_name(path)).wrap());
495 });
496}
497
498fn draw_group_header(ui: &mut egui::Ui, heading: &str, import_filter: &[&str], project: &Project) {
499 ui.horizontal(|ui| {
500 ui.label(heading);
501 if ui.small_button("Import...").clicked() {
502 if let Some(path) = rfd::FileDialog::new().add_filter(heading, import_filter).pick_file() {
503 if let Err(e) = project.import_asset(&path) {
504 eprintln!("Failed to import asset: {e}");
505 }
506 }
507 }
508 });
509}
510
511fn draw_texture_tile(ui: &mut egui::Ui, project: &Project, path: &Path, previews: &mut HashMap<PathBuf, egui::TextureHandle>) {
515 let handle = previews
516 .entry(path.to_path_buf())
517 .or_insert_with(|| load_texture_preview(ui.ctx(), &project.root.join(path)));
518
519 let fitted_size = fit_within_square(handle.size_vec2(), ASSET_PREVIEW_SIZE);
523
524 ui.vertical(|ui| {
525 ui.set_width(ASSET_PREVIEW_SIZE);
526 let (slot_rect, _) = ui.allocate_exact_size(egui::vec2(ASSET_PREVIEW_SIZE, ASSET_PREVIEW_SIZE), egui::Sense::hover());
527 let image_rect = egui::Rect::from_center_size(slot_rect.center(), fitted_size);
528 egui::Image::new((handle.id(), fitted_size)).paint_at(ui, image_rect);
529 ui.add(egui::Label::new(asset_file_name(path)).wrap());
530 });
531}
532
533fn fit_within_square(size: egui::Vec2, max_side: f32) -> egui::Vec2 {
536 if size.x <= 0.0 || size.y <= 0.0 {
537 return egui::Vec2::splat(max_side);
538 }
539 let scale = (max_side / size.x).min(max_side / size.y).min(1.0);
540 size * scale
541}
542
543fn load_texture_preview(ctx: &egui::Context, full_path: &Path) -> egui::TextureHandle {
544 let color_image = image::open(full_path)
545 .map(|image| {
546 let thumbnail = image.thumbnail(64, 64).to_rgba8();
547 let size = [thumbnail.width() as usize, thumbnail.height() as usize];
548 egui::ColorImage::from_rgba_unmultiplied(size, thumbnail.as_raw())
549 })
550 .unwrap_or_else(|e| {
551 eprintln!("Failed to load texture preview for {}: {e}", full_path.display());
552 egui::ColorImage::filled([1, 1], egui::Color32::from_gray(80))
553 });
554
555 ctx.load_texture(full_path.display().to_string(), color_image, egui::TextureOptions::LINEAR)
556}
557
558fn draw_model_tile(ui: &mut egui::Ui, path: &Path) {
561 ui.vertical(|ui| {
562 ui.set_width(ASSET_PREVIEW_SIZE);
563 let (rect, _) = ui.allocate_exact_size(egui::vec2(ASSET_PREVIEW_SIZE, ASSET_PREVIEW_SIZE), egui::Sense::hover());
564 ui.painter().rect_filled(rect, 4.0, egui::Color32::from_gray(55));
565 ui.painter().text(
566 rect.center(),
567 egui::Align2::CENTER_CENTER,
568 "OBJ",
569 egui::FontId::proportional(14.0),
570 egui::Color32::from_gray(200),
571 );
572 ui.add(egui::Label::new(asset_file_name(path)).wrap());
573 });
574}
575
576fn draw_script_tile(
582 ui: &mut egui::Ui,
583 project: &Project,
584 path: &Path,
585 settings: &mut EditorSettings,
586 renaming_script: &mut Option<PathBuf>,
587 script_rename_buffer: &mut String,
588 requests: &mut UiRequests,
589) {
590 ui.vertical(|ui| {
591 ui.set_width(ASSET_PREVIEW_SIZE);
592
593 if renaming_script.as_deref() == Some(path) {
594 let response = ui.text_edit_singleline(script_rename_buffer);
595 if response.lost_focus() {
596 requests.rename_script = Some((path.to_path_buf(), script_rename_buffer.clone()));
602 *renaming_script = None;
603 } else {
604 response.request_focus();
605 }
606 return;
607 }
608
609 let (rect, response) =
610 ui.allocate_exact_size(egui::vec2(ASSET_PREVIEW_SIZE, ASSET_PREVIEW_SIZE), egui::Sense::click());
611 ui.painter().rect_filled(rect, 4.0, egui::Color32::from_gray(55));
612 ui.painter().text(
613 rect.center(),
614 egui::Align2::CENTER_CENTER,
615 "RHAI",
616 egui::FontId::proportional(12.0),
617 egui::Color32::from_gray(200),
618 );
619
620 if response.clicked() {
625 open_script_in_editor(project, path, settings);
626 }
627
628 response.context_menu(|ui| {
629 if ui.button("Rename").clicked() {
630 *renaming_script = Some(path.to_path_buf());
631 *script_rename_buffer = path.file_stem().map(|s| s.to_string_lossy().into_owned()).unwrap_or_default();
632 ui.close();
633 }
634 if ui.button("Change Editor...").clicked() {
635 pick_and_save_editor(settings);
636 ui.close();
637 }
638 });
639
640 ui.add(egui::Label::new(asset_file_name(path)).wrap());
641 });
642}
643
644fn open_script_in_editor(project: &Project, path: &Path, settings: &mut EditorSettings) {
647 if settings.preferred_editor.is_none() {
648 pick_and_save_editor(settings);
649 }
650 let Some(editor) = settings.preferred_editor.as_ref() else { return };
651
652 if let Err(e) = std::process::Command::new(editor).arg(project.root.join(path)).spawn() {
653 eprintln!("Failed to launch {}: {e}", editor.display());
654 }
655}
656
657fn pick_and_save_editor(settings: &mut EditorSettings) {
661 let Some(editor) = rfd::FileDialog::new().set_title("Choose a text editor").pick_file() else { return };
662 settings.preferred_editor = Some(editor);
663 if let Err(e) = settings.save() {
664 eprintln!("Failed to save editor preference: {e}");
665 }
666}
667
668fn asset_file_name(path: &Path) -> String {
669 path.file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default()
670}
671
672fn draw_inspector(
673 ui: &mut egui::Ui,
674 world: &mut World,
675 selected: &mut Option<Entity>,
676 project: Option<&Project>,
677 entity_assets: &mut HashMap<Entity, RenderableAsset>,
678 is_playing: bool,
679 requests: &mut UiRequests,
680) {
681 if let Some(entity) = *selected
682 && !world.is_alive(entity)
683 {
684 *selected = None;
685 }
686
687 egui::Panel::right("inspector_panel").show(ui, |ui| {
688 ui.heading("Inspector");
689 ui.separator();
690
691 let Some(entity) = *selected else {
692 ui.label("No entity selected.");
693 return;
694 };
695
696 ui.add_enabled_ui(!is_playing, |ui| {
699 draw_transform_editor(ui, world, entity, requests);
700 });
701 ui.separator();
702 draw_camera_editor(ui, world, entity, requests);
703 ui.separator();
704 draw_renderable_editor(ui, world, entity, project, entity_assets, requests);
705 ui.separator();
706 draw_script_editor(ui, world, entity, project, requests);
707 });
708}
709
710fn draw_transform_editor(ui: &mut egui::Ui, world: &mut World, entity: Entity, requests: &mut UiRequests) {
711 let Some(transform) = world.transforms.get_mut(entity) else { return };
712 let mut edited = false;
713
714 ui.label("Position");
715 ui.horizontal(|ui| {
716 edited |= ui.add(egui::DragValue::new(&mut transform.position.x).speed(0.05).prefix("x: ")).changed();
717 edited |= ui.add(egui::DragValue::new(&mut transform.position.y).speed(0.05).prefix("y: ")).changed();
718 edited |= ui.add(egui::DragValue::new(&mut transform.position.z).speed(0.05).prefix("z: ")).changed();
719 });
720
721 let (mut yaw, mut pitch, mut roll) = transform.rotation.to_euler(glam::EulerRot::YXZ);
722 yaw = yaw.to_degrees();
723 pitch = pitch.to_degrees();
724 roll = roll.to_degrees();
725 ui.label("Rotation (deg)");
726 let mut changed = false;
727 ui.horizontal(|ui| {
728 changed |= ui.add(egui::DragValue::new(&mut pitch).speed(0.5).prefix("x: ")).changed();
729 changed |= ui.add(egui::DragValue::new(&mut yaw).speed(0.5).prefix("y: ")).changed();
730 changed |= ui.add(egui::DragValue::new(&mut roll).speed(0.5).prefix("z: ")).changed();
731 });
732 if changed {
733 transform.rotation = glam::Quat::from_euler(
734 glam::EulerRot::YXZ,
735 yaw.to_radians(),
736 pitch.to_radians(),
737 roll.to_radians(),
738 );
739 }
740 edited |= changed;
741
742 ui.label("Scale");
743 ui.horizontal(|ui| {
744 edited |= ui.add(egui::DragValue::new(&mut transform.scale.x).speed(0.05).prefix("x: ")).changed();
745 edited |= ui.add(egui::DragValue::new(&mut transform.scale.y).speed(0.05).prefix("y: ")).changed();
746 edited |= ui.add(egui::DragValue::new(&mut transform.scale.z).speed(0.05).prefix("z: ")).changed();
747 });
748
749 if edited {
750 requests.edited = true;
751 }
752}
753
754fn draw_camera_editor(ui: &mut egui::Ui, world: &mut World, entity: Entity, requests: &mut UiRequests) {
760 let mut enabled = world.cameras.get(entity).is_some();
761 if ui.checkbox(&mut enabled, "Camera").changed() {
762 if enabled {
763 world.set_camera(entity, CameraComponent::default());
764 } else {
765 world.clear_camera(entity);
766 }
767 requests.edited = true;
768 }
769 if !enabled {
770 return;
771 }
772
773 let mut activate: Option<bool> = None;
774 if let Some(component) = world.cameras.get_mut(entity) {
775 let mut edited = false;
776 ui.horizontal(|ui| {
777 edited |= ui.add(egui::DragValue::new(&mut component.fov).speed(0.5).prefix("FOV: ").range(1.0..=179.0)).changed();
778 });
779 ui.horizontal(|ui| {
780 edited |= ui.add(egui::DragValue::new(&mut component.znear).speed(0.01).prefix("Near: ")).changed();
781 edited |= ui.add(egui::DragValue::new(&mut component.zfar).speed(0.5).prefix("Far: ")).changed();
782 });
783
784 let mut active = component.active;
785 if ui.checkbox(&mut active, "Active Camera (used in Play)").changed() {
786 activate = Some(active);
787 edited = true;
788 }
789 if edited {
790 requests.edited = true;
791 }
792 }
793
794 match activate {
797 Some(true) => {
798 for (other, component) in world.cameras.iter_mut() {
799 component.active = other == entity;
800 }
801 }
802 Some(false) => {
803 if let Some(component) = world.cameras.get_mut(entity) {
804 component.active = false;
805 }
806 }
807 None => {}
808 }
809}
810
811fn draw_renderable_editor(
812 ui: &mut egui::Ui,
813 world: &mut World,
814 entity: Entity,
815 project: Option<&Project>,
816 entity_assets: &mut HashMap<Entity, RenderableAsset>,
817 requests: &mut UiRequests,
818) {
819 let kind = world.renderables.get(entity).map(|renderable| match renderable {
820 Renderable::Sprite(_) => "Sprite",
821 Renderable::Model(_) => "Model",
822 });
823 ui.label(format!("Renderable: {}", kind.unwrap_or("<none>")));
824
825 if let Some(asset) = entity_assets.get(&entity) {
826 let path = match asset {
827 RenderableAsset::Sprite { texture_path, .. } => texture_path,
828 RenderableAsset::Model { model_path } => model_path,
829 };
830 ui.label(format!("Asset: {}", path.display()));
831 if ui.button("Remove").clicked() {
832 world.clear_renderable(entity);
833 entity_assets.remove(&entity);
834 requests.edited = true;
835 return;
836 }
837 ui.label("Change to:");
838 }
839
840 let Some(project) = project else {
841 ui.label("Open or create a project to attach a renderable.");
842 return;
843 };
844
845 ui.collapsing("Attach Sprite", |ui| {
846 for path in project.list_assets(RenderableKind::Sprite) {
847 if ui.button(path.display().to_string()).clicked() {
848 requests.attach_renderable = Some((entity, RenderableKind::Sprite, path));
849 }
850 }
851 });
852 ui.collapsing("Attach Model", |ui| {
853 for path in project.list_assets(RenderableKind::Model) {
854 if ui.button(path.display().to_string()).clicked() {
855 requests.attach_renderable = Some((entity, RenderableKind::Model, path));
856 }
857 }
858 });
859}
860
861fn draw_script_editor(ui: &mut egui::Ui, world: &mut World, entity: Entity, project: Option<&Project>, requests: &mut UiRequests) {
867 ui.label("Scripts");
868
869 let mut move_up: Option<usize> = None;
870 let mut move_down: Option<usize> = None;
871 let mut remove: Option<usize> = None;
872
873 if let Some(list) = world.scripts.get_mut(entity) {
874 let last = list.0.len().saturating_sub(1);
875 for (index, attachment) in list.0.iter_mut().enumerate() {
876 ui.horizontal(|ui| {
877 if ui.checkbox(&mut attachment.enabled, "").changed() {
878 requests.edited = true;
879 }
880 ui.label(asset_file_name(&attachment.path));
881 ui.add_enabled_ui(index > 0, |ui| {
884 if ui.small_button("Up").clicked() {
885 move_up = Some(index);
886 }
887 });
888 ui.add_enabled_ui(index < last, |ui| {
889 if ui.small_button("Down").clicked() {
890 move_down = Some(index);
891 }
892 });
893 if ui.small_button("Remove").clicked() {
894 remove = Some(index);
895 }
896 });
897 }
898 }
899
900 if let Some(list) = world.scripts.get_mut(entity) {
901 if let Some(index) = move_up.filter(|&i| i > 0) {
902 list.0.swap(index, index - 1);
903 requests.edited = true;
904 }
905 if let Some(index) = move_down.filter(|&i| i + 1 < list.0.len()) {
906 list.0.swap(index, index + 1);
907 requests.edited = true;
908 }
909 if let Some(index) = remove {
910 list.0.remove(index);
911 requests.edited = true;
912 }
913 }
914
915 let Some(project) = project else {
916 ui.label("Open or create a project to attach a script.");
917 return;
918 };
919
920 ui.collapsing("Attach Script", |ui| {
921 let scripts = project.list_scripts();
922 if scripts.is_empty() {
923 ui.label("No .rhai files in this project's scripts/ folder.");
924 }
925 for path in scripts {
926 if ui.button(path.display().to_string()).clicked() {
927 let attachment = ScriptAttachment { path, enabled: true };
928 match world.scripts.get_mut(entity) {
929 Some(list) => list.0.push(attachment),
930 None => world.scripts.insert(entity, ScriptList(vec![attachment])),
931 }
932 requests.edited = true;
933 }
934 }
935 });
936}
937
938fn draw_export_status_overlay(ui: &mut egui::Ui, export_status: Option<&str>) {
942 let Some(message) = export_status else { return };
943
944 egui::Area::new(egui::Id::new("export_status_overlay"))
945 .anchor(egui::Align2::RIGHT_BOTTOM, egui::vec2(-8.0, -8.0))
946 .show(ui.ctx(), |ui| {
947 egui::Frame::popup(ui.style()).show(ui, |ui| {
948 ui.set_max_width(420.0);
949 ui.label(message);
950 });
951 });
952}
953
954fn draw_script_error_overlay(ui: &mut egui::Ui, script_errors: &[String]) {
957 if script_errors.is_empty() {
958 return;
959 }
960
961 egui::Area::new(egui::Id::new("script_error_overlay"))
962 .anchor(egui::Align2::LEFT_BOTTOM, egui::vec2(8.0, -8.0))
963 .show(ui.ctx(), |ui| {
964 egui::Frame::popup(ui.style()).show(ui, |ui| {
965 ui.set_max_width(420.0);
966 for message in script_errors {
967 ui.colored_label(egui::Color32::from_rgb(220, 90, 90), message);
968 }
969 });
970 });
971}