diff --git a/Cargo.lock b/Cargo.lock index 416e908b..e13e33ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1133,6 +1133,7 @@ dependencies = [ "egui", "egui_demo_lib", "egui_extras", + "egui_glow", "ehttp", "image", "poll-promise", diff --git a/egui_demo_app/Cargo.toml b/egui_demo_app/Cargo.toml index f0f47b70..b68cfc0b 100644 --- a/egui_demo_app/Cargo.toml +++ b/egui_demo_app/Cargo.toml @@ -38,6 +38,7 @@ chrono = { version = "0.4", features = ["js-sys", "wasmbind"] } eframe = { version = "0.17.0", path = "../eframe" } egui = { version = "0.17.0", path = "../egui", features = ["extra_debug_asserts"] } egui_demo_lib = { version = "0.17.0", path = "../egui_demo_lib", features = ["chrono"] } +egui_glow = { version = "0.17.0", path = "../egui_glow" } # Optional dependencies: diff --git a/egui_demo_app/src/apps/custom3d.rs b/egui_demo_app/src/apps/custom3d.rs new file mode 100644 index 00000000..5d239fe2 --- /dev/null +++ b/egui_demo_app/src/apps/custom3d.rs @@ -0,0 +1,183 @@ +use std::sync::Arc; + +use egui::mutex::Mutex; +use egui_glow::glow; + +pub struct Custom3d { + /// Behind an `Arc>` so we can pass it to [`egui::PaintCallback`] and paint later. + rotating_triangle: Arc>, + angle: f32, +} + +impl Custom3d { + pub fn new(gl: &glow::Context) -> Self { + Self { + rotating_triangle: Arc::new(Mutex::new(RotatingTriangle::new(gl))), + angle: 0.0, + } + } +} + +impl eframe::App for Custom3d { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + egui::CentralPanel::default().show(ctx, |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing.x = 0.0; + ui.label("The triangle is being painted using "); + ui.hyperlink_to("glow", "https://github.com/grovesNL/glow"); + ui.label(" (OpenGL)."); + }); + ui.label( + "It's not a very impressive demo, but it shows you can embed 3D inside of egui.", + ); + + egui::Frame::canvas(ui.style()).show(ui, |ui| { + self.custom_painting(ui); + }); + ui.label("Drag to rotate!"); + ui.add(egui_demo_lib::egui_github_link_file!()); + }); + } + + fn on_exit(&mut self, gl: &glow::Context) { + self.rotating_triangle.lock().destroy(gl); + } +} + +impl Custom3d { + fn custom_painting(&mut self, ui: &mut egui::Ui) { + let (rect, response) = + ui.allocate_exact_size(egui::Vec2::splat(300.0), egui::Sense::drag()); + + self.angle += response.drag_delta().x * 0.01; + + // Clone locals so we can move them into the paint callback: + let angle = self.angle; + let rotating_triangle = self.rotating_triangle.clone(); + + let callback = egui::PaintCallback { + rect, + callback: std::sync::Arc::new(move |_info, render_ctx| { + if let Some(painter) = render_ctx.downcast_ref::() { + rotating_triangle.lock().paint(painter.gl(), angle); + } else { + eprintln!("Can't do custom painting because we are not using a glow context"); + } + }), + }; + ui.painter().add(callback); + } +} + +struct RotatingTriangle { + program: glow::Program, + vertex_array: glow::VertexArray, +} + +#[allow(unsafe_code)] // we need unsafe code to use glow +impl RotatingTriangle { + fn new(gl: &glow::Context) -> Self { + use glow::HasContext as _; + + let shader_version = if cfg!(target_arch = "wasm32") { + "#version 300 es" + } else { + "#version 410" + }; + + unsafe { + let program = gl.create_program().expect("Cannot create program"); + + let (vertex_shader_source, fragment_shader_source) = ( + r#" + const vec2 verts[3] = vec2[3]( + vec2(0.0, 1.0), + vec2(-1.0, -1.0), + vec2(1.0, -1.0) + ); + const vec4 colors[3] = vec4[3]( + vec4(1.0, 0.0, 0.0, 1.0), + vec4(0.0, 1.0, 0.0, 1.0), + vec4(0.0, 0.0, 1.0, 1.0) + ); + out vec4 v_color; + uniform float u_angle; + void main() { + v_color = colors[gl_VertexID]; + gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0); + gl_Position.x *= cos(u_angle); + } + "#, + r#" + precision mediump float; + in vec4 v_color; + out vec4 out_color; + void main() { + out_color = v_color; + } + "#, + ); + + let shader_sources = [ + (glow::VERTEX_SHADER, vertex_shader_source), + (glow::FRAGMENT_SHADER, fragment_shader_source), + ]; + + let shaders: Vec<_> = shader_sources + .iter() + .map(|(shader_type, shader_source)| { + let shader = gl + .create_shader(*shader_type) + .expect("Cannot create shader"); + gl.shader_source(shader, &format!("{}\n{}", shader_version, shader_source)); + gl.compile_shader(shader); + if !gl.get_shader_compile_status(shader) { + panic!("{}", gl.get_shader_info_log(shader)); + } + gl.attach_shader(program, shader); + shader + }) + .collect(); + + gl.link_program(program); + if !gl.get_program_link_status(program) { + panic!("{}", gl.get_program_info_log(program)); + } + + for shader in shaders { + gl.detach_shader(program, shader); + gl.delete_shader(shader); + } + + let vertex_array = gl + .create_vertex_array() + .expect("Cannot create vertex array"); + + Self { + program, + vertex_array, + } + } + } + + fn destroy(&self, gl: &glow::Context) { + use glow::HasContext as _; + unsafe { + gl.delete_program(self.program); + gl.delete_vertex_array(self.vertex_array); + } + } + + fn paint(&self, gl: &glow::Context, angle: f32) { + use glow::HasContext as _; + unsafe { + gl.use_program(Some(self.program)); + gl.uniform_1_f32( + gl.get_uniform_location(self.program, "u_angle").as_ref(), + angle, + ); + gl.bind_vertex_array(Some(self.vertex_array)); + gl.draw_arrays(glow::TRIANGLES, 0, 3); + } + } +} diff --git a/egui_demo_app/src/apps/mod.rs b/egui_demo_app/src/apps/mod.rs index 4476d06b..e7d652d5 100644 --- a/egui_demo_app/src/apps/mod.rs +++ b/egui_demo_app/src/apps/mod.rs @@ -1,9 +1,9 @@ +mod custom3d; mod fractal_clock; - #[cfg(feature = "http")] mod http_app; +pub use custom3d::Custom3d; pub use fractal_clock::FractalClock; - #[cfg(feature = "http")] pub use http_app::HttpApp; diff --git a/egui_demo_app/src/wrap_app.rs b/egui_demo_app/src/wrap_app.rs index 958ab84e..b0ce4f37 100644 --- a/egui_demo_app/src/wrap_app.rs +++ b/egui_demo_app/src/wrap_app.rs @@ -1,3 +1,5 @@ +use egui_glow::glow; + #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] struct EasyMarkApp { @@ -41,6 +43,7 @@ impl eframe::App for FractalClockApp { }); } } + // ---------------------------------------------------------------------------- #[derive(Default)] @@ -67,71 +70,90 @@ impl eframe::App for ColorTestApp { // ---------------------------------------------------------------------------- -/// All the different demo apps. +/// The state that we persist (serialize). #[derive(Default)] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", serde(default))] -pub struct Apps { +pub struct State { demo: DemoApp, easy_mark_editor: EasyMarkApp, #[cfg(feature = "http")] http: crate::apps::HttpApp, clock: FractalClockApp, color_test: ColorTestApp, + + selected_anchor: String, + backend_panel: super::backend_panel::BackendPanel, } -impl Apps { - fn iter_mut(&mut self) -> impl Iterator { +/// Wraps many demo/test apps into one. +pub struct WrapApp { + state: State, + // not serialized (because it contains OpenGL buffers etc) + custom3d: crate::apps::Custom3d, + dropped_files: Vec, +} + +impl WrapApp { + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + let mut slf = Self { + state: State::default(), + custom3d: crate::apps::Custom3d::new(&cc.gl), + dropped_files: Default::default(), + }; + + #[cfg(feature = "persistence")] + if let Some(storage) = cc.storage { + if let Some(state) = eframe::get_value(storage, eframe::APP_KEY) { + slf.state = state; + } + } + + slf + } + + fn apps_iter_mut(&mut self) -> impl Iterator { vec![ - ("✨ Demos", "demo", &mut self.demo as &mut dyn eframe::App), + ( + "✨ Demos", + "demo", + &mut self.state.demo as &mut dyn eframe::App, + ), ( "🖹 EasyMark editor", "easymark", - &mut self.easy_mark_editor as &mut dyn eframe::App, + &mut self.state.easy_mark_editor as &mut dyn eframe::App, ), #[cfg(feature = "http")] - ("⬇ HTTP", "http", &mut self.http as &mut dyn eframe::App), + ( + "⬇ HTTP", + "http", + &mut self.state.http as &mut dyn eframe::App, + ), ( "🕑 Fractal Clock", "clock", - &mut self.clock as &mut dyn eframe::App, + &mut self.state.clock as &mut dyn eframe::App, + ), + ( + "🔺 3D painting", + "custom3e", + &mut self.custom3d as &mut dyn eframe::App, ), ( "🎨 Color test", "colors", - &mut self.color_test as &mut dyn eframe::App, + &mut self.state.color_test as &mut dyn eframe::App, ), ] .into_iter() } } -/// Wraps many demo/test apps into one. -#[derive(Default)] -#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] -#[cfg_attr(feature = "serde", serde(default))] -pub struct WrapApp { - selected_anchor: String, - apps: Apps, - backend_panel: super::backend_panel::BackendPanel, - #[cfg_attr(feature = "serde", serde(skip))] - dropped_files: Vec, -} - -impl WrapApp { - pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { - #[cfg(feature = "persistence")] - if let Some(storage) = _cc.storage { - return eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default(); - } - Self::default() - } -} - impl eframe::App for WrapApp { #[cfg(feature = "persistence")] fn save(&mut self, storage: &mut dyn eframe::Storage) { - eframe::set_value(storage, eframe::APP_KEY, self); + eframe::set_value(storage, eframe::APP_KEY, &self.state); } fn clear_color(&self) -> egui::Rgba { @@ -141,12 +163,13 @@ impl eframe::App for WrapApp { fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { if let Some(web_info) = frame.info().web_info.as_ref() { if let Some(anchor) = web_info.location.hash.strip_prefix('#') { - self.selected_anchor = anchor.to_owned(); + self.state.selected_anchor = anchor.to_owned(); } } - if self.selected_anchor.is_empty() { - self.selected_anchor = self.apps.iter_mut().next().unwrap().0.to_owned(); + if self.state.selected_anchor.is_empty() { + let selected_anchor = self.apps_iter_mut().next().unwrap().0.to_owned(); + self.state.selected_anchor = selected_anchor; } egui::TopBottomPanel::top("wrap_app_top_bar").show(ctx, |ui| { @@ -154,11 +177,11 @@ impl eframe::App for WrapApp { self.bar_contents(ui, frame); }); - self.backend_panel.update(ctx, frame); + self.state.backend_panel.update(ctx, frame); - if self.backend_panel.open || ctx.memory().everything_is_visible() { + if self.state.backend_panel.open || ctx.memory().everything_is_visible() { egui::SidePanel::left("backend_panel").show(ctx, |ui| { - self.backend_panel.ui(ui, frame); + self.state.backend_panel.ui(ui, frame); ui.separator(); @@ -172,7 +195,7 @@ impl eframe::App for WrapApp { } if ui.button("Reset everything").clicked() { - *self = Default::default(); + self.state = Default::default(); *ui.ctx().memory() = Default::default(); } }); @@ -181,21 +204,26 @@ impl eframe::App for WrapApp { let mut found_anchor = false; - for (_name, anchor, app) in self.apps.iter_mut() { - if anchor == self.selected_anchor || ctx.memory().everything_is_visible() { + let selected_anchor = self.state.selected_anchor.clone(); + for (_name, anchor, app) in self.apps_iter_mut() { + if anchor == selected_anchor || ctx.memory().everything_is_visible() { app.update(ctx, frame); found_anchor = true; } } if !found_anchor { - self.selected_anchor = "demo".into(); + self.state.selected_anchor = "demo".into(); } - self.backend_panel.end_of_frame(ctx); + self.state.backend_panel.end_of_frame(ctx); self.ui_file_drag_and_drop(ctx); } + + fn on_exit(&mut self, gl: &glow::Context) { + self.custom3d.on_exit(gl); + } } impl WrapApp { @@ -205,27 +233,29 @@ impl WrapApp { ui.horizontal_wrapped(|ui| { egui::widgets::global_dark_light_mode_switch(ui); - ui.checkbox(&mut self.backend_panel.open, "💻 Backend"); + ui.checkbox(&mut self.state.backend_panel.open, "💻 Backend"); ui.separator(); - for (name, anchor, _app) in self.apps.iter_mut() { + let mut selected_anchor = self.state.selected_anchor.clone(); + for (name, anchor, _app) in self.apps_iter_mut() { if ui - .selectable_label(self.selected_anchor == anchor, name) + .selectable_label(selected_anchor == anchor, name) .clicked() { - self.selected_anchor = anchor.to_owned(); + selected_anchor = anchor.to_owned(); if frame.is_web() { ui.output().open_url(format!("#{}", anchor)); } } } + self.state.selected_anchor = selected_anchor; ui.with_layout(egui::Layout::right_to_left(), |ui| { if false { // TODO: fix the overlap on small screens if let Some(seconds_since_midnight) = crate::seconds_since_midnight() { if clock_button(ui, seconds_since_midnight).clicked() { - self.selected_anchor = "clock".to_owned(); + self.state.selected_anchor = "clock".to_owned(); if frame.is_web() { ui.output().open_url("#clock"); } diff --git a/egui_demo_lib/src/demo/widget_gallery.rs b/egui_demo_lib/src/demo/widget_gallery.rs index c9045fef..329012fe 100644 --- a/egui_demo_lib/src/demo/widget_gallery.rs +++ b/egui_demo_lib/src/demo/widget_gallery.rs @@ -53,7 +53,7 @@ impl super::Demo for WidgetGallery { egui::Window::new(self.name()) .open(open) .resizable(true) - .default_width(300.0) + .default_width(280.0) .show(ctx, |ui| { use super::View as _; self.ui(ui); diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index 10667e4d..68d374ff 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -20,6 +20,27 @@ pub use glow::Context; const VERT_SRC: &str = include_str!("shader/vertex.glsl"); const FRAG_SRC: &str = include_str!("shader/fragment.glsl"); +#[derive(Copy, Clone)] +pub enum TextureFilter { + Linear, + Nearest, +} + +impl Default for TextureFilter { + fn default() -> Self { + TextureFilter::Linear + } +} + +impl TextureFilter { + pub(crate) fn glow_code(&self) -> u32 { + match self { + TextureFilter::Linear => glow::LINEAR, + TextureFilter::Nearest => glow::NEAREST, + } + } +} + /// An OpenGL painter using [`glow`]. /// /// This is responsible for painting egui and managing egui textures. @@ -56,27 +77,6 @@ pub struct Painter { destroyed: bool, } -#[derive(Copy, Clone)] -pub enum TextureFilter { - Linear, - Nearest, -} - -impl Default for TextureFilter { - fn default() -> Self { - TextureFilter::Linear - } -} - -impl TextureFilter { - pub(crate) fn glow_code(&self) -> u32 { - match self { - TextureFilter::Linear => glow::LINEAR, - TextureFilter::Nearest => glow::NEAREST, - } - } -} - impl Painter { /// Create painter. ///