use crate::winit; use crossbeam_channel::Sender; use cyborg::camera::Flycam; use std::path::PathBuf; #[derive(Clone, Debug)] pub enum FileEvent { Save, SaveAs(PathBuf), Import(ImportKind, PathBuf), } #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum ImportKind { Stl, Gltf, } #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Workspace { Scene, NodeEditor, } pub struct UserInterface { file_sender: Sender, developer_mode: bool, show_profiler: bool, quit: bool, show_about: bool, show_log: bool, log_contents: String, workspace: Workspace, } impl UserInterface { pub fn new(file_sender: Sender) -> Self { Self { file_sender, developer_mode: true, show_profiler: false, show_log: false, quit: false, show_about: false, log_contents: "Hello logging!\n".to_string(), workspace: Workspace::Scene, } } pub fn should_quit(&self) -> bool { self.quit } pub fn run( &mut self, ctx: &egui::Context, viewport: &mut ViewportWidget, objects: &mut [ObjectWidget], ) { egui::TopBottomPanel::top("menu_bar").show(ctx, |ui| { egui::menu::bar(ui, |ui| self.ui_menu_bar(ui)); }); if self.show_profiler { self.show_profiler = puffin_egui::profiler_window(ctx); } if self.show_about { egui::Window::new("About") .open(&mut self.show_about) .resizable(false) .collapsible(false) .show(ctx, |ui| { ui.vertical_centered(|ui| { ui.heading("Cyborg Editor"); }); }); } if self.show_log { egui::TopBottomPanel::bottom("info_panel") .resizable(true) .show(ctx, |ui| { ui.heading("Log Output"); egui::containers::ScrollArea::vertical() .auto_shrink([false, false]) .max_width(f32::INFINITY) .max_height(f32::INFINITY) .show(ui, |ui| { let text_edit = egui::TextEdit::multiline(&mut self.log_contents) .desired_width(f32::INFINITY) .frame(false); ui.add(text_edit); }); }); } match self.workspace { Workspace::Scene => self.ui_scene(ctx, viewport, objects), Workspace::NodeEditor => { egui::CentralPanel::default().show(ctx, |ui| self.ui_node_editor(ui)); } } } pub fn ui_menu_bar(&mut self, ui: &mut egui::Ui) { ui.menu_button("File", |ui| { if self.developer_mode { if ui.button("fuck").clicked() { println!("fuck"); } } if ui.button("Save").clicked() { ui.close_menu(); self.file_sender.send(FileEvent::Save).unwrap(); } if ui.button("Save as...").clicked() { ui.close_menu(); let file_sender = self.file_sender.to_owned(); std::thread::spawn(move || { if let Some(path) = rfd::FileDialog::new().save_file() { file_sender.send(FileEvent::SaveAs(path)).unwrap(); } }); } ui.menu_button("Import...", |ui| { if ui.button("STL").clicked() { ui.close_menu(); self.on_import(ImportKind::Stl); } if ui.button("glTF").clicked() { ui.close_menu(); self.on_import(ImportKind::Gltf); } }); if ui.button("Open...").clicked() { println!("Opening!"); ui.close_menu(); } if ui.button("Quit").clicked() { println!("Quitting!"); ui.close_menu(); self.quit = true; } }); ui.menu_button("Edit", |ui| { if ui.button("Undo").clicked() { println!("Undoing!"); } if ui.button("Redo").clicked() { println!("Redoing!"); } }); ui.menu_button("View", |ui| { ui.checkbox(&mut self.show_log, "Log"); ui.checkbox(&mut self.developer_mode, "Developer mode"); if ui.checkbox(&mut self.show_profiler, "Profiler").changed() { puffin_egui::puffin::set_scopes_on(self.show_profiler); } }); ui.menu_button("Help", |ui| { if ui.button("About").clicked() { self.show_about = true; ui.close_menu(); } }); ui.separator(); self.ui_select_workspace(ui); } pub fn ui_select_workspace(&mut self, ui: &mut egui::Ui) { ui.selectable_value(&mut self.workspace, Workspace::Scene, "Scene"); ui.selectable_value(&mut self.workspace, Workspace::NodeEditor, "Node Editor"); } pub fn ui_scene( &mut self, ctx: &egui::Context, viewport: &mut ViewportWidget, objects: &mut [ObjectWidget], ) { egui::SidePanel::left("objects_panel") .resizable(true) .show(ctx, |ui| { egui::ScrollArea::vertical().show(ui, |ui| { for (index, object) in objects.iter_mut().enumerate() { object.ui(index, ui); } }); }); egui::CentralPanel::default().show(ctx, |ui| { ui.add(viewport); ui.heading("Viewport"); }); } pub fn ui_node_editor(&mut self, ui: &mut egui::Ui) { ui.label("Node editor goes here!"); } pub fn on_import(&self, kind: ImportKind) { let kind_name = match kind { ImportKind::Stl => "STL", ImportKind::Gltf => "glTF", }; let extensions: &[&str] = match kind { ImportKind::Stl => &["stl"], ImportKind::Gltf => &["gltf", "glb", "vrm"], }; let file_sender = self.file_sender.to_owned(); std::thread::spawn(move || { let dialog = rfd::FileDialog::new().add_filter(kind_name, extensions); if let Some(paths) = dialog.pick_files() { for path in paths.iter() { let event = FileEvent::Import(kind, path.into()); file_sender.send(event).unwrap(); } } }); } } pub struct ViewportWidget { pub texture: egui::TextureId, pub flycam: Flycam, pub width: u32, pub height: u32, } impl ViewportWidget { pub fn new(texture: egui::TextureId) -> Self { Self { texture, flycam: Flycam::new(0.002, 10.0, 0.25), width: 640, height: 480, } } } impl egui::Widget for &mut ViewportWidget { fn ui(self, ui: &mut egui::Ui) -> egui::Response { ui.style_mut().spacing.window_margin = egui::style::Margin::same(0.0); let rect = ui.max_rect(); let id = egui::Id::new("viewport_widget"); let sense = egui::Sense::click_and_drag(); let response = ui.interact(rect, id, sense); self.width = rect.width().round() as u32; self.height = rect.height().round() as u32; use egui::{pos2, Color32, Mesh, Rect, Shape}; let mut mesh = Mesh::with_texture(self.texture); let uv = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)); let tint = Color32::WHITE; mesh.add_rect_with_uv(rect, uv, tint); ui.painter().add(Shape::mesh(mesh)); if response.dragged() { let delta = response.drag_delta(); self.flycam.process_mouse(delta.x as f64, delta.y as f64); for event in ui.input().events.iter() { match event { egui::Event::Key { key, pressed, .. } => { use winit::event::{ElementState, VirtualKeyCode}; let key = match key { egui::Key::W => Some(VirtualKeyCode::W), egui::Key::A => Some(VirtualKeyCode::A), egui::Key::S => Some(VirtualKeyCode::S), egui::Key::D => Some(VirtualKeyCode::D), // TODO remap from shift key somehow? egui::Key::E => Some(VirtualKeyCode::E), egui::Key::Q => Some(VirtualKeyCode::Q), _ => None, }; let state = if *pressed { ElementState::Pressed } else { ElementState::Released }; if let Some(key) = key { self.flycam.process_keyboard(key, state); } } _ => {} } } } else if response.drag_released() { self.flycam.defocus(); } self.flycam.resize(self.width, self.height); self.flycam.update(); response } } pub struct ObjectWidget { pub name: Option, pub transform: crate::model::Transform, pub entities: Vec, pub children: Vec, pub dirty: bool, pub children_dirty: bool, } impl ObjectWidget { pub fn ui(&mut self, index: usize, ui: &mut egui::Ui) { egui::CollapsingHeader::new(self.name.as_ref().unwrap_or(&"".into())) .id_source(format!("child_{}", index)) .default_open(true) .show(ui, |ui| { self.ui_self(ui); for (index, child) in self.children.iter_mut().enumerate() { child.ui(index, ui); if child.dirty || child.children_dirty { self.children_dirty = true; } } }); } pub fn ui_self(&mut self, ui: &mut egui::Ui) { egui::Grid::new("root_object") .num_columns(4) .striped(true) .show(ui, |ui| { ui.label("Position: "); self.ui_position(ui); ui.end_row(); ui.label("Rotation: "); self.ui_rotation(ui); ui.end_row(); ui.label("Scale: "); self.ui_scale(ui); ui.end_row(); }); } pub fn ui_position(&mut self, ui: &mut egui::Ui) { let speed = 0.1 * self.transform.scale; if Self::ui_vec3(ui, speed, &mut self.transform.position) { self.dirty = true; } } pub fn ui_rotation(&mut self, ui: &mut egui::Ui) { let axes = &mut self.transform.rotation; if ui.drag_angle(&mut axes.x).changed() { self.dirty = true; } if ui.drag_angle(&mut axes.y).changed() { self.dirty = true; } if ui.drag_angle(&mut axes.z).changed() { self.dirty = true; } } pub fn ui_vec3(ui: &mut egui::Ui, speed: f32, vec3: &mut glam::Vec3) -> bool { let x_drag = egui::DragValue::new(&mut vec3.x).speed(speed); let dirty = ui.add(x_drag).changed(); let dirty = dirty || { // For some reason, Rust complains if this isn't in a block let y_drag = egui::DragValue::new(&mut vec3.y).speed(speed); ui.add(y_drag).changed() }; let z_drag = egui::DragValue::new(&mut vec3.z).speed(speed); let dirty = dirty || ui.add(z_drag).changed(); dirty } pub fn ui_scale(&mut self, ui: &mut egui::Ui) { let scale_speed = self.transform.scale * 0.01; let drag = egui::DragValue::new(&mut self.transform.scale) .clamp_range(0.0..=f32::INFINITY) .speed(scale_speed); if ui.add(drag).changed() { self.dirty = true; } } pub fn flush_dirty(&mut self, mut f: impl FnMut(&mut Self)) { let mut stack = vec![self]; while let Some(parent) = stack.pop() { if parent.dirty { parent.dirty = false; f(parent); } if parent.children_dirty { parent.children_dirty = false; for child in parent.children.iter_mut() { stack.push(child); } } } } }