cyborg/editor/src/ui.rs

433 lines
13 KiB
Rust

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<FileEvent>,
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<FileEvent>) -> 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<String>,
pub transform: crate::model::Transform,
pub entities: Vec<legion::Entity>,
pub children: Vec<ObjectWidget>,
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(&"<unnamed>".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);
}
}
}
}
}