From 877e89f2eca93deae59ea7f0850dfbe000e6dc35 Mon Sep 17 00:00:00 2001 From: AlexApps99 Date: Tue, 19 Oct 2021 10:13:32 +1300 Subject: [PATCH] Add egui_glow backend as alternative to egui_glium (#685) --- ARCHITECTURE.md | 4 + README.md | 7 +- eframe/CHANGELOG.md | 1 + egui_glow/CHANGELOG.md | 8 + egui_glow/Cargo.toml | 70 ++++ egui_glow/README.md | 16 + egui_glow/examples/pure.rs | 130 ++++++ egui_glow/src/backend.rs | 394 ++++++++++++++++++ egui_glow/src/lib.rs | 226 +++++++++++ egui_glow/src/painter.rs | 622 +++++++++++++++++++++++++++++ egui_glow/src/persistence.rs | 95 +++++ egui_glow/src/shader/fragment.glsl | 73 ++++ egui_glow/src/shader/vertex.glsl | 42 ++ 13 files changed, 1685 insertions(+), 3 deletions(-) create mode 100644 egui_glow/CHANGELOG.md create mode 100644 egui_glow/Cargo.toml create mode 100644 egui_glow/README.md create mode 100644 egui_glow/examples/pure.rs create mode 100644 egui_glow/src/backend.rs create mode 100644 egui_glow/src/lib.rs create mode 100644 egui_glow/src/painter.rs create mode 100644 egui_glow/src/persistence.rs create mode 100644 egui_glow/src/shader/fragment.glsl create mode 100644 egui_glow/src/shader/vertex.glsl diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index f9b8b0cc18ed..110c062b3a24 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -36,6 +36,10 @@ Puts an egui app inside the web browser by compiling to WASM and binding to the ### `egui_glium` Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glium](https://github.com/glium/glium). +### `egui_glow` +Puts an egui app inside a native window on your laptop. Paints the triangles that egui outputs using [glow](https://github.com/grovesNL/glow). +An alternative to `egui_glium`, not used by `eframe` at this time. + ### `eframe` A wrapper around `egui_web` + `egui_glium`, so you can compile the same app for either web or native. diff --git a/README.md b/README.md index 9329b633f55a..56f3778c9199 100644 --- a/README.md +++ b/README.md @@ -163,11 +163,12 @@ The integration needs to do two things: ### Official -I maintain two official egui integrations made for apps: +There are three official egui integrations made for apps: * [`egui_web`](https://github.com/emilk/egui/tree/master/egui_web) for making a web app. Compiles to WASM, renders with WebGL. [Click to run the egui demo](https://emilk.github.io/egui/index.html). * [`egui_glium`](https://github.com/emilk/egui/tree/master/egui_glium) for compiling native apps with [Glium](https://github.com/glium/glium). -* [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit) for integrating with [`winit`](https://github.com/rust-windowing/winit). `egui-winit` is used by `egui_glium`. +* [`egui_glow`](https://github.com/emilk/egui/tree/master/egui_glow) for compiling native apps with [Glow](https://github.com/grovesNL/glow). +* [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit) for integrating with [`winit`](https://github.com/rust-windowing/winit). `egui-winit` is used by `egui_glium` and `egui_glow`. If you making an app, consider using [`eframe`](https://github.com/emilk/egui/tree/master/eframe), a framework which allows you to write code that works on both the web (`egui_web`) and native (using `egui_glium`). @@ -211,7 +212,7 @@ loop { } ``` -For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs) or [the `egui_web` `WebGL` painter](https://github.com/emilk/egui/blob/master/egui_web/src/webgl1.rs). +For a reference OpenGL backend, see [the `egui_glium` painter](https://github.com/emilk/egui/blob/master/egui_glium/src/painter.rs), [the `egui_glow` painter](https://github.com/emilk/egui/blob/master/egui_glow/src/painter.rs), or [the `egui_web` `WebGL` painter](https://github.com/emilk/egui/blob/master/egui_web/src/webgl1.rs). ### Debugging your integration diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index cbe9bea7e229..05c997344d0e 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -9,6 +9,7 @@ NOTE: [`egui_web`](egui_web/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.m * Remove "http" feature (use https://github.com/emilk/ehttp instead!). * Increase native scroll speed. * Add `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted. +* Add new backend `egui_glow` as an alternative to `egui_glium` (not yet exposed as a feature flag) ## 0.14.0 - 2021-08-24 diff --git a/egui_glow/CHANGELOG.md b/egui_glow/CHANGELOG.md new file mode 100644 index 000000000000..ed8a1e9b4f44 --- /dev/null +++ b/egui_glow/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog for egui_glow +All notable changes to the `egui_glow` integration will be noted in this file. + + +## Unreleased +`egui_glow` has been newly created, with feature parity to `egui_glium`. +As `glow` is a set of lower-level bindings to OpenGL, this crate is potentially less stable than `egui_glium`, +but there are no known issues, and the crate will only become more stable over time, if any issues manifest. diff --git a/egui_glow/Cargo.toml b/egui_glow/Cargo.toml new file mode 100644 index 000000000000..340c7b50cf4f --- /dev/null +++ b/egui_glow/Cargo.toml @@ -0,0 +1,70 @@ +[package] +name = "egui_glow" +version = "0.14.0" +authors = ["Emil Ernerfeldt "] +description = "Bindings for using egui natively using the glow library" +edition = "2018" +homepage = "https://github.com/emilk/egui/tree/master/egui_glow" +license = "MIT OR Apache-2.0" +readme = "README.md" +repository = "https://github.com/emilk/egui/tree/master/egui_glow" +categories = ["gui", "game-development"] +keywords = ["glow", "egui", "gui", "gamedev"] +include = [ + "../LICENSE-APACHE", + "../LICENSE-MIT", + "**/*.rs", + "Cargo.toml", + "src/shader/*.glsl", +] + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +egui = { version = "0.14.0", path = "../egui", default-features = false, features = ["single_threaded"] } +egui-winit = { version = "0.14.0", path = "../egui-winit", default-features = false } +epi = { version = "0.14.0", path = "../epi" } +glutin = "0.27" +glow = "0.11" +memoffset = "0.6" + +# feature "persistence": +directories-next = { version = "2", optional = true } +ron = { version = "0.6", optional = true } +serde = { version = "1", optional = true } + +# feature "time" +chrono = { version = "0.4", optional = true } + +[dev-dependencies] +image = { version = "0.23", default-features = false, features = ["png"] } + +[features] +default = ["clipboard", "default_fonts", "links"] + +# enable cut/copy/paste to OS clipboard. +# if disabled a clipboard will be simulated so you can still copy/paste within the egui app. +clipboard = ["egui-winit/clipboard"] + +# If set, egui will use `include_bytes!` to bundle some fonts. +# If you plan on specifying your own fonts you may disable this feature. +default_fonts = ["egui/default_fonts"] + +# enable opening links in a browser when an egui hyperlink is clicked. +links = ["egui-winit/links"] + +persistence = [ + "directories-next", + "egui-winit/serialize", + "egui/persistence", + "epi/persistence", + "ron", + "serde", +] + +# experimental support for a screen reader +screen_reader = ["egui-winit/screen_reader"] + +# for seconds_since_midnight (used in egui_demo_lib) +time = ["chrono"] diff --git a/egui_glow/README.md b/egui_glow/README.md new file mode 100644 index 000000000000..080d10ea6439 --- /dev/null +++ b/egui_glow/README.md @@ -0,0 +1,16 @@ +# egui_glow + +[![Latest version](https://img.shields.io/crates/v/egui_glow.svg)](https://crates.io/crates/egui_glow) +[![Documentation](https://docs.rs/egui_glow/badge.svg)](https://docs.rs/egui_glow) +![MIT](https://img.shields.io/badge/license-MIT-blue.svg) +![Apache](https://img.shields.io/badge/license-Apache-blue.svg) + +This crates provides bindings between [`egui`](https://github.com/emilk/egui) and [glow](https://crates.io/crates/glow) which allows you to write GUI code using egui and compile it and run it natively, cross platform. + +To use on Linux, first run: + +``` +sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev +``` + +This crate depends on [`egui-winit`](https://github.com/emilk/egui/tree/master/egui-winit). diff --git a/egui_glow/examples/pure.rs b/egui_glow/examples/pure.rs new file mode 100644 index 000000000000..fc2c688697c5 --- /dev/null +++ b/egui_glow/examples/pure.rs @@ -0,0 +1,130 @@ +//! Example how to use pure `egui_glow` without [`epi`]. + +fn create_display( + event_loop: &glutin::event_loop::EventLoop<()>, +) -> ( + glutin::WindowedContext, + glow::Context, +) { + let window_builder = glutin::window::WindowBuilder::new() + .with_resizable(true) + .with_inner_size(glutin::dpi::LogicalSize { + width: 800.0, + height: 600.0, + }) + .with_title("egui_glow example"); + + let gl_window = unsafe { + glutin::ContextBuilder::new() + .with_depth_buffer(0) + .with_srgb(true) + .with_stencil_buffer(0) + .with_vsync(true) + .build_windowed(window_builder, event_loop) + .unwrap() + .make_current() + .unwrap() + }; + + let gl = unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) }; + + unsafe { + use glow::HasContext; + gl.enable(glow::FRAMEBUFFER_SRGB); + } + + (gl_window, gl) +} + +fn main() { + let event_loop = glutin::event_loop::EventLoop::with_user_event(); + let (gl_window, gl) = create_display(&event_loop); + + let mut egui = egui_glow::EguiGlow::new(&gl_window, &gl); + + event_loop.run(move |event, _, control_flow| { + let mut redraw = || { + egui.begin_frame(gl_window.window()); + + let mut quit = false; + + egui::SidePanel::left("my_side_panel").show(egui.ctx(), |ui| { + ui.heading("Hello World!"); + if ui.button("Quit").clicked() { + quit = true; + } + + egui::ComboBox::from_label("Version") + .width(150.0) + .selected_text("foo") + .show_ui(ui, |ui| { + egui::CollapsingHeader::new("Dev") + .default_open(true) + .show(ui, |ui| { + ui.label("contents"); + }); + }); + }); + + let (needs_repaint, shapes) = egui.end_frame(gl_window.window()); + + *control_flow = if quit { + glutin::event_loop::ControlFlow::Exit + } else if needs_repaint { + gl_window.window().request_redraw(); + glutin::event_loop::ControlFlow::Poll + } else { + glutin::event_loop::ControlFlow::Wait + }; + + { + let clear_color = egui::Rgba::from_rgb(0.1, 0.3, 0.2); + unsafe { + use glow::HasContext; + gl.clear_color( + clear_color[0], + clear_color[1], + clear_color[2], + clear_color[3], + ); + gl.clear(glow::COLOR_BUFFER_BIT); + } + + // draw things behind egui here + + egui.paint(&gl_window, &gl, shapes); + + // draw things on top of egui here + + gl_window.swap_buffers().unwrap(); + } + }; + + match event { + // Platform-dependent event handlers to workaround a winit bug + // See: https://github.com/rust-windowing/winit/issues/987 + // See: https://github.com/rust-windowing/winit/issues/1619 + glutin::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(), + glutin::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(), + + glutin::event::Event::WindowEvent { event, .. } => { + if egui.is_quit_event(&event) { + *control_flow = glutin::event_loop::ControlFlow::Exit; + } + + if let glutin::event::WindowEvent::Resized(physical_size) = event { + gl_window.resize(physical_size); + } + + egui.on_event(&event); + + gl_window.window().request_redraw(); // TODO: ask egui if the events warrants a repaint instead + } + glutin::event::Event::LoopDestroyed => { + egui.destroy(&gl); + } + + _ => (), + } + }); +} diff --git a/egui_glow/src/backend.rs b/egui_glow/src/backend.rs new file mode 100644 index 000000000000..2a7310ac7baf --- /dev/null +++ b/egui_glow/src/backend.rs @@ -0,0 +1,394 @@ +use crate::*; +use egui::Color32; +use egui_winit::WindowSettings; +#[cfg(target_os = "windows")] +use glutin::platform::windows::WindowBuilderExtWindows; +use std::time::Instant; + +#[cfg(feature = "persistence")] +const EGUI_MEMORY_KEY: &str = "egui"; +#[cfg(feature = "persistence")] +const WINDOW_KEY: &str = "window"; + +#[cfg(feature = "persistence")] +fn deserialize_window_settings(storage: &Option>) -> Option { + epi::get_value(&**storage.as_ref()?, WINDOW_KEY) +} + +#[cfg(not(feature = "persistence"))] +fn deserialize_window_settings(_: &Option>) -> Option { + None +} + +#[cfg(feature = "persistence")] +fn deserialize_memory(storage: &Option>) -> Option { + epi::get_value(&**storage.as_ref()?, EGUI_MEMORY_KEY) +} + +#[cfg(not(feature = "persistence"))] +fn deserialize_memory(_: &Option>) -> Option { + None +} + +impl epi::TextureAllocator for Painter { + fn alloc_srgba_premultiplied( + &mut self, + size: (usize, usize), + srgba_pixels: &[Color32], + ) -> egui::TextureId { + let id = self.alloc_user_texture(); + self.set_user_texture(id, size, srgba_pixels); + id + } + + fn free(&mut self, id: egui::TextureId) { + self.free_user_texture(id) + } +} + +struct RequestRepaintEvent; + +struct GlowRepaintSignal(std::sync::Mutex>); + +impl epi::RepaintSignal for GlowRepaintSignal { + fn request_repaint(&self) { + self.0.lock().unwrap().send_event(RequestRepaintEvent).ok(); + } +} + +#[cfg(target_os = "windows")] +fn window_builder_drag_and_drop( + window_builder: glutin::window::WindowBuilder, + enable: bool, +) -> glutin::window::WindowBuilder { + window_builder.with_drag_and_drop(enable) +} + +#[cfg(not(target_os = "windows"))] +fn window_builder_drag_and_drop( + window_builder: glutin::window::WindowBuilder, + _enable: bool, +) -> glutin::window::WindowBuilder { + // drag and drop can only be disabled on windows + window_builder +} + +#[allow(unsafe_code)] +fn create_display( + app: &dyn epi::App, + native_options: &epi::NativeOptions, + window_settings: &Option, + window_icon: Option, + event_loop: &glutin::event_loop::EventLoop, +) -> ( + glutin::WindowedContext, + glow::Context, +) { + let mut window_builder = glutin::window::WindowBuilder::new() + .with_always_on_top(native_options.always_on_top) + .with_maximized(native_options.maximized) + .with_decorations(native_options.decorated) + .with_resizable(native_options.resizable) + .with_title(app.name()) + .with_transparent(native_options.transparent) + .with_window_icon(window_icon); + + window_builder = + window_builder_drag_and_drop(window_builder, native_options.drag_and_drop_support); + + let initial_size_points = native_options.initial_window_size; + + if let Some(window_settings) = window_settings { + window_builder = window_settings.initialize_window(window_builder); + } else if let Some(initial_size_points) = initial_size_points { + window_builder = window_builder.with_inner_size(glutin::dpi::LogicalSize { + width: initial_size_points.x as f64, + height: initial_size_points.y as f64, + }); + } + + let gl_window = unsafe { + glutin::ContextBuilder::new() + .with_depth_buffer(0) + .with_srgb(true) + .with_stencil_buffer(0) + .with_vsync(true) + .build_windowed(window_builder, event_loop) + .unwrap() + .make_current() + .unwrap() + }; + + let gl = unsafe { glow::Context::from_loader_function(|s| gl_window.get_proc_address(s)) }; + + unsafe { + use glow::HasContext; + gl.enable(glow::FRAMEBUFFER_SRGB); + } + + (gl_window, gl) +} + +#[cfg(not(feature = "persistence"))] +fn create_storage(_app_name: &str) -> Option> { + None +} + +#[cfg(feature = "persistence")] +fn create_storage(app_name: &str) -> Option> { + if let Some(proj_dirs) = directories_next::ProjectDirs::from("", "", app_name) { + let data_dir = proj_dirs.data_dir().to_path_buf(); + if let Err(err) = std::fs::create_dir_all(&data_dir) { + eprintln!( + "Saving disabled: Failed to create app path at {:?}: {}", + data_dir, err + ); + None + } else { + let mut config_dir = data_dir; + config_dir.push("app.ron"); + let storage = crate::persistence::FileStorage::from_path(config_dir); + Some(Box::new(storage)) + } + } else { + eprintln!("Saving disabled: Failed to find path to data_dir."); + None + } +} + +fn integration_info( + window: &glutin::window::Window, + previous_frame_time: Option, +) -> epi::IntegrationInfo { + epi::IntegrationInfo { + web_info: None, + prefer_dark_mode: None, // TODO: figure out system default + cpu_usage: previous_frame_time, + seconds_since_midnight: seconds_since_midnight(), + native_pixels_per_point: Some(native_pixels_per_point(window)), + } +} + +fn load_icon(icon_data: epi::IconData) -> Option { + glutin::window::Icon::from_rgba(icon_data.rgba, icon_data.width, icon_data.height).ok() +} + +// ---------------------------------------------------------------------------- + +/// Run an egui app +#[allow(unsafe_code)] +pub fn run(mut app: Box, native_options: &epi::NativeOptions) -> ! { + #[allow(unused_mut)] + let mut storage = create_storage(app.name()); + + let window_settings = deserialize_window_settings(&storage); + let event_loop = glutin::event_loop::EventLoop::with_user_event(); + let icon = native_options.icon_data.clone().and_then(load_icon); + let (gl_window, gl) = + create_display(&*app, native_options, &window_settings, icon, &event_loop); + + let repaint_signal = std::sync::Arc::new(GlowRepaintSignal(std::sync::Mutex::new( + event_loop.create_proxy(), + ))); + + let mut egui = EguiGlow::new(&gl_window, &gl); + *egui.ctx().memory() = deserialize_memory(&storage).unwrap_or_default(); + + { + let (ctx, painter) = egui.ctx_and_painter_mut(); + let mut app_output = epi::backend::AppOutput::default(); + let mut frame = epi::backend::FrameBuilder { + info: integration_info(gl_window.window(), None), + tex_allocator: painter, + output: &mut app_output, + repaint_signal: repaint_signal.clone(), + } + .build(); + app.setup(ctx, &mut frame, storage.as_deref()); + } + + let mut previous_frame_time = None; + + let mut is_focused = true; + + #[cfg(feature = "persistence")] + let mut last_auto_save = Instant::now(); + + if app.warm_up_enabled() { + let saved_memory = egui.ctx().memory().clone(); + egui.ctx().memory().set_everything_is_visible(true); + + egui.begin_frame(gl_window.window()); + let (ctx, painter) = egui.ctx_and_painter_mut(); + let mut app_output = epi::backend::AppOutput::default(); + let mut frame = epi::backend::FrameBuilder { + info: integration_info(gl_window.window(), None), + tex_allocator: painter, + output: &mut app_output, + repaint_signal: repaint_signal.clone(), + } + .build(); + + app.update(ctx, &mut frame); + + let _ = egui.end_frame(gl_window.window()); + + *egui.ctx().memory() = saved_memory; // We don't want to remember that windows were huge. + egui.ctx().clear_animations(); + + // TODO: handle app_output + // eprintln!("Warmed up in {} ms", warm_up_start.elapsed().as_millis()) + } + + event_loop.run(move |event, _, control_flow| { + let mut redraw = || { + if !is_focused { + // On Mac, a minimized Window uses up all CPU: https://github.com/emilk/egui/issues/325 + // We can't know if we are minimized: https://github.com/rust-windowing/winit/issues/208 + // But we know if we are focused (in foreground). When minimized, we are not focused. + // However, a user may want an egui with an animation in the background, + // so we still need to repaint quite fast. + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + let frame_start = std::time::Instant::now(); + + egui.begin_frame(gl_window.window()); + let (ctx, painter) = egui.ctx_and_painter_mut(); + let mut app_output = epi::backend::AppOutput::default(); + let mut frame = epi::backend::FrameBuilder { + info: integration_info(gl_window.window(), previous_frame_time), + tex_allocator: painter, + output: &mut app_output, + repaint_signal: repaint_signal.clone(), + } + .build(); + app.update(ctx, &mut frame); + let (needs_repaint, shapes) = egui.end_frame(gl_window.window()); + + let frame_time = (Instant::now() - frame_start).as_secs_f64() as f32; + previous_frame_time = Some(frame_time); + + { + let clear_color = app.clear_color(); + unsafe { + use glow::HasContext; + gl.disable(glow::SCISSOR_TEST); + gl.clear_color( + clear_color[0], + clear_color[1], + clear_color[2], + clear_color[3], + ); + gl.clear(glow::COLOR_BUFFER_BIT); + } + egui.paint(&gl_window, &gl, shapes); + gl_window.swap_buffers().unwrap(); + } + + { + let epi::backend::AppOutput { + quit, + window_size, + decorated, + drag_window, + } = app_output; + + if let Some(decorated) = decorated { + gl_window.window().set_decorations(decorated); + } + + if let Some(window_size) = window_size { + gl_window.window().set_inner_size( + glutin::dpi::PhysicalSize { + width: (egui.ctx().pixels_per_point() * window_size.x).round(), + height: (egui.ctx().pixels_per_point() * window_size.y).round(), + } + .to_logical::(native_pixels_per_point(gl_window.window()) as f64), + ); + } + + if drag_window { + let _ = gl_window.window().drag_window(); + } + + *control_flow = if quit { + glutin::event_loop::ControlFlow::Exit + } else if needs_repaint { + gl_window.window().request_redraw(); + glutin::event_loop::ControlFlow::Poll + } else { + glutin::event_loop::ControlFlow::Wait + }; + } + + #[cfg(feature = "persistence")] + if let Some(storage) = &mut storage { + let now = Instant::now(); + if now - last_auto_save > app.auto_save_interval() { + if app.persist_native_window() { + epi::set_value( + storage.as_mut(), + WINDOW_KEY, + &WindowSettings::from_display(gl_window.window()), + ); + } + if app.persist_egui_memory() { + epi::set_value(storage.as_mut(), EGUI_MEMORY_KEY, &*egui.ctx().memory()); + } + app.save(storage.as_mut()); + storage.flush(); + last_auto_save = now; + } + } + }; + + match event { + // Platform-dependent event handlers to workaround a winit bug + // See: https://github.com/rust-windowing/winit/issues/987 + // See: https://github.com/rust-windowing/winit/issues/1619 + glutin::event::Event::RedrawEventsCleared if cfg!(windows) => redraw(), + glutin::event::Event::RedrawRequested(_) if !cfg!(windows) => redraw(), + + glutin::event::Event::WindowEvent { event, .. } => { + if egui.is_quit_event(&event) { + *control_flow = glutin::event_loop::ControlFlow::Exit; + } + + if let glutin::event::WindowEvent::Focused(new_focused) = event { + is_focused = new_focused; + } + + egui.on_event(&event); + + gl_window.window().request_redraw(); // TODO: ask egui if the events warrants a repaint instead + } + glutin::event::Event::LoopDestroyed => { + app.on_exit(); + #[cfg(feature = "persistence")] + if let Some(storage) = &mut storage { + if app.persist_native_window() { + epi::set_value( + storage.as_mut(), + WINDOW_KEY, + &WindowSettings::from_display(gl_window.window()), + ); + } + if app.persist_egui_memory() { + epi::set_value(storage.as_mut(), EGUI_MEMORY_KEY, &*egui.ctx().memory()); + } + app.save(storage.as_mut()); + storage.flush(); + } + + egui.destroy(&gl); + } + + glutin::event::Event::UserEvent(RequestRepaintEvent) => { + gl_window.window().request_redraw(); + } + + _ => (), + } + }); +} diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs new file mode 100644 index 000000000000..79c61ce11b89 --- /dev/null +++ b/egui_glow/src/lib.rs @@ -0,0 +1,226 @@ +//! [`egui`] bindings for [`glow`](https://github.com/grovesNL/glow). +//! +//! The main type you want to use is [`EguiGlow`]. +//! +//! This library is an [`epi`] backend. +//! If you are writing an app, you may want to look at [`eframe`](https://docs.rs/eframe) instead. + +// Forbid warnings in release builds: +#![cfg_attr(not(debug_assertions), deny(warnings))] +#![deny(unsafe_code)] +#![warn( + clippy::all, + clippy::await_holding_lock, + clippy::char_lit_as_u8, + clippy::checked_conversions, + clippy::dbg_macro, + clippy::debug_assert_with_mut_call, + clippy::doc_markdown, + clippy::empty_enum, + clippy::enum_glob_use, + clippy::exit, + clippy::expl_impl_clone_on_copy, + clippy::explicit_deref_methods, + clippy::explicit_into_iter_loop, + clippy::fallible_impl_from, + clippy::filter_map_next, + clippy::float_cmp_const, + clippy::fn_params_excessive_bools, + clippy::if_let_mutex, + clippy::imprecise_flops, + clippy::inefficient_to_string, + clippy::invalid_upcast_comparisons, + clippy::large_types_passed_by_value, + clippy::let_unit_value, + clippy::linkedlist, + clippy::lossy_float_literal, + clippy::macro_use_imports, + clippy::manual_ok_or, + clippy::map_err_ignore, + clippy::map_flatten, + clippy::match_on_vec_items, + clippy::match_same_arms, + clippy::match_wildcard_for_single_variants, + clippy::mem_forget, + clippy::mismatched_target_os, + clippy::missing_errors_doc, + clippy::missing_safety_doc, + clippy::mut_mut, + clippy::mutex_integer, + clippy::needless_borrow, + clippy::needless_continue, + clippy::needless_pass_by_value, + clippy::option_option, + clippy::path_buf_push_overwrite, + clippy::ptr_as_ptr, + clippy::ref_option_ref, + clippy::rest_pat_in_fully_bound_structs, + clippy::same_functions_in_if_condition, + clippy::string_add_assign, + clippy::string_add, + clippy::string_lit_as_bytes, + clippy::string_to_string, + clippy::todo, + clippy::trait_duplication_in_bounds, + clippy::unimplemented, + clippy::unnested_or_patterns, + clippy::unused_self, + clippy::useless_transmute, + clippy::verbose_file_reads, + clippy::zero_sized_map_values, + future_incompatible, + missing_crate_level_docs, + nonstandard_style, + rust_2018_idioms +)] +#![allow(clippy::float_cmp)] +#![allow(clippy::manual_range_contains)] + +mod backend; +mod painter; +#[cfg(feature = "persistence")] +pub mod persistence; + +pub use backend::*; +pub use painter::Painter; + +pub use egui_winit; +pub use epi::NativeOptions; + +// ---------------------------------------------------------------------------- + +/// Time of day as seconds since midnight. Used for clock in demo app. +pub fn seconds_since_midnight() -> Option { + #[cfg(feature = "time")] + { + use chrono::Timelike; + let time = chrono::Local::now().time(); + let seconds_since_midnight = + time.num_seconds_from_midnight() as f64 + 1e-9 * (time.nanosecond() as f64); + Some(seconds_since_midnight) + } + #[cfg(not(feature = "time"))] + None +} + +pub fn screen_size_in_pixels(window: &glutin::window::Window) -> egui::Vec2 { + let glutin::dpi::PhysicalSize { width, height } = window.inner_size(); + egui::vec2(width as f32, height as f32) +} + +pub fn native_pixels_per_point(window: &glutin::window::Window) -> f32 { + window.scale_factor() as f32 +} + +// ---------------------------------------------------------------------------- + +/// Use [`egui`] from a [`glow`] app. +pub struct EguiGlow { + egui_ctx: egui::CtxRef, + egui_winit: egui_winit::State, + painter: crate::Painter, +} + +impl EguiGlow { + pub fn new( + gl_window: &glutin::WindowedContext, + gl: &glow::Context, + ) -> Self { + Self { + egui_ctx: Default::default(), + egui_winit: egui_winit::State::new(gl_window.window()), + painter: crate::Painter::new(gl), + } + } + + pub fn ctx(&self) -> &egui::CtxRef { + &self.egui_ctx + } + + pub fn painter_mut(&mut self) -> &mut crate::Painter { + &mut self.painter + } + + pub fn ctx_and_painter_mut(&mut self) -> (&egui::CtxRef, &mut crate::Painter) { + (&self.egui_ctx, &mut self.painter) + } + + pub fn pixels_per_point(&self) -> f32 { + self.egui_winit.pixels_per_point() + } + + pub fn egui_input(&self) -> &egui::RawInput { + self.egui_winit.egui_input() + } + + /// Returns `true` if egui wants exclusive use of this event + /// (e.g. a mouse click on an egui window, or entering text into a text field). + /// For instance, if you use egui for a game, you want to first call this + /// and only when this returns `false` pass on the events to your game. + /// + /// Note that egui uses `tab` to move focus between elements, so this will always return `true` for tabs. + pub fn on_event(&mut self, event: &glutin::event::WindowEvent<'_>) -> bool { + self.egui_winit.on_event(&self.egui_ctx, event) + } + + /// Is this a close event or a Cmd-Q/Alt-F4 keyboard command? + pub fn is_quit_event(&self, event: &glutin::event::WindowEvent<'_>) -> bool { + self.egui_winit.is_quit_event(event) + } + + pub fn begin_frame(&mut self, window: &glutin::window::Window) { + let raw_input = self.take_raw_input(window); + self.begin_frame_with_input(raw_input); + } + + pub fn begin_frame_with_input(&mut self, raw_input: egui::RawInput) { + self.egui_ctx.begin_frame(raw_input); + } + + /// Prepare for a new frame. Normally you would call [`Self::begin_frame`] instead. + pub fn take_raw_input(&mut self, window: &glutin::window::Window) -> egui::RawInput { + self.egui_winit.take_egui_input(window) + } + + /// Returns `needs_repaint` and shapes to draw. + pub fn end_frame( + &mut self, + window: &glutin::window::Window, + ) -> (bool, Vec) { + let (egui_output, shapes) = self.egui_ctx.end_frame(); + let needs_repaint = egui_output.needs_repaint; + self.handle_output(window, egui_output); + (needs_repaint, shapes) + } + + pub fn handle_output(&mut self, window: &glutin::window::Window, output: egui::Output) { + self.egui_winit + .handle_output(window, &self.egui_ctx, output); + } + + pub fn paint( + &mut self, + gl_window: &glutin::WindowedContext, + gl: &glow::Context, + shapes: Vec, + ) { + let clipped_meshes = self.egui_ctx.tessellate(shapes); + self.painter.paint_meshes( + gl_window, + gl, + self.egui_ctx.pixels_per_point(), + clipped_meshes, + &self.egui_ctx.texture(), + ); + } + + #[cfg(debug_assertions)] + pub fn destroy(&mut self, gl: &glow::Context) { + self.painter.destroy(gl) + } + + #[cfg(not(debug_assertions))] + pub fn destroy(&self, gl: &glow::Context) { + self.painter.destroy(gl) + } +} diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs new file mode 100644 index 000000000000..c1b87a49b9fb --- /dev/null +++ b/egui_glow/src/painter.rs @@ -0,0 +1,622 @@ +#![allow(unsafe_code)] + +use egui::{ + emath::Rect, + epaint::{Color32, Mesh, Vertex}, +}; +use memoffset::offset_of; + +use std::convert::TryInto; + +use glow::HasContext; + +const VERT_SRC: &str = include_str!("shader/vertex.glsl"); +const FRAG_SRC: &str = include_str!("shader/fragment.glsl"); + +fn srgbtexture2d(gl: &glow::Context, data: &[u8], w: usize, h: usize) -> glow::NativeTexture { + assert_eq!(data.len(), w * h * 4); + assert!(w >= 1); + assert!(h >= 1); + unsafe { + let tex = gl.create_texture().unwrap(); + gl.bind_texture(glow::TEXTURE_2D, Some(tex)); + + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_MAG_FILTER, + glow::LINEAR as i32, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_WRAP_S, + glow::CLAMP_TO_EDGE as i32, + ); + gl.tex_parameter_i32( + glow::TEXTURE_2D, + glow::TEXTURE_WRAP_T, + glow::CLAMP_TO_EDGE as i32, + ); + + gl.tex_storage_2d(glow::TEXTURE_2D, 1, glow::SRGB8_ALPHA8, w as i32, h as i32); + gl.tex_sub_image_2d( + glow::TEXTURE_2D, + 0, + 0, + 0, + w as i32, + h as i32, + glow::RGBA, + glow::UNSIGNED_BYTE, + glow::PixelUnpackData::Slice(data), + ); + + assert_eq!(gl.get_error(), glow::NO_ERROR, "OpenGL error occurred!"); + tex + } +} + +unsafe fn as_u8_slice(s: &[T]) -> &[u8] { + std::slice::from_raw_parts(s.as_ptr().cast::(), s.len() * std::mem::size_of::()) +} + +/// OpenGL painter +/// +/// This struct must be destroyed with [`Painter::destroy`] before dropping, to ensure OpenGL +/// objects have been properly deleted and are not leaked. +pub struct Painter { + program: glow::NativeProgram, + u_screen_size: glow::UniformLocation, + u_sampler: glow::UniformLocation, + egui_texture: Option, + egui_texture_version: Option, + + /// `None` means unallocated (freed) slot. + user_textures: Vec>, + + vertex_array: glow::NativeVertexArray, + vertex_buffer: glow::NativeBuffer, + element_array_buffer: glow::NativeBuffer, + + // Stores outdated OpenGL textures that are yet to be deleted + old_textures: Vec, + // Only in debug builds, to make sure we are destroyed correctly. + #[cfg(debug_assertions)] + destroyed: bool, +} + +#[derive(Default)] +struct UserTexture { + /// Pending upload (will be emptied later). + /// This is the format glow likes. + data: Vec, + size: (usize, usize), + + /// Lazily uploaded + gl_texture: Option, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +#[allow(dead_code)] +enum ShaderVersion { + Gl120, + Gl140, + Es100, + Es300, +} + +impl ShaderVersion { + fn get(gl: &glow::Context) -> Self { + Self::parse(unsafe { &gl.get_parameter_string(glow::SHADING_LANGUAGE_VERSION) }) + } + + #[inline] + fn parse(glsl_ver: &str) -> Self { + let start = glsl_ver.find(|c| char::is_ascii_digit(&c)).unwrap(); + let es = glsl_ver[..start].contains(" ES "); + let ver = glsl_ver[start..].splitn(2, ' ').next().unwrap(); + let [maj, min]: [u8; 2] = ver + .splitn(3, '.') + .take(2) + .map(|x| x.parse().unwrap_or_default()) + .collect::>() + .try_into() + .unwrap(); + if es { + if maj >= 3 { + Self::Es300 + } else { + Self::Es100 + } + } else if maj > 1 || (maj == 1 && min >= 40) { + Self::Gl140 + } else { + Self::Gl120 + } + } + + fn version(&self) -> &'static str { + match self { + Self::Gl120 => "#version 120\n", + Self::Gl140 => "#version 140\n", + Self::Es100 => "#version 100\n", + Self::Es300 => "#version 300 es\n", + } + } +} + +#[test] +fn test_shader_version() { + use ShaderVersion::{Es100, Es300, Gl120, Gl140}; + for (s, v) in [ + ("1.2 OpenGL foo bar", Gl120), + ("3.0", Gl140), + ("0.0", Gl120), + ("OpenGL ES GLSL 3.00 (WebGL2)", Es300), + ("OpenGL ES GLSL 1.00 (WebGL)", Es100), + ("OpenGL ES GLSL ES 1.00 foo bar", Es100), + ("WebGL GLSL ES 3.00 foo bar", Es300), + ("WebGL GLSL ES 3.00", Es300), + ("WebGL GLSL ES 1.0 foo bar", Es100), + ] { + assert_eq!(ShaderVersion::parse(s), v); + } +} + +impl Painter { + pub fn new(gl: &glow::Context) -> Painter { + let header = ShaderVersion::get(gl).version(); + let mut v_src = header.to_owned(); + v_src.push_str(VERT_SRC); + let mut f_src = header.to_owned(); + f_src.push_str(FRAG_SRC); + unsafe { + let v = gl.create_shader(glow::VERTEX_SHADER).unwrap(); + gl.shader_source(v, &v_src); + gl.compile_shader(v); + if !gl.get_shader_compile_status(v) { + panic!( + "Failed to compile vertex shader: {}", + gl.get_shader_info_log(v) + ); + } + + let f = gl.create_shader(glow::FRAGMENT_SHADER).unwrap(); + gl.shader_source(f, &f_src); + gl.compile_shader(f); + if !gl.get_shader_compile_status(f) { + panic!( + "Failed to compile fragment shader: {}", + gl.get_shader_info_log(f) + ); + } + + let program = gl.create_program().unwrap(); + gl.attach_shader(program, v); + gl.attach_shader(program, f); + gl.link_program(program); + if !gl.get_program_link_status(program) { + panic!("{}", gl.get_program_info_log(program)); + } + gl.detach_shader(program, v); + gl.detach_shader(program, f); + gl.delete_shader(v); + gl.delete_shader(f); + + let u_screen_size = gl.get_uniform_location(program, "u_screen_size").unwrap(); + let u_sampler = gl.get_uniform_location(program, "u_sampler").unwrap(); + + let vertex_array = gl.create_vertex_array().unwrap(); + let vertex_buffer = gl.create_buffer().unwrap(); + let element_array_buffer = gl.create_buffer().unwrap(); + + gl.bind_vertex_array(Some(vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer)); + + let a_pos_loc = gl.get_attrib_location(program, "a_pos").unwrap(); + let a_tc_loc = gl.get_attrib_location(program, "a_tc").unwrap(); + let a_srgba_loc = gl.get_attrib_location(program, "a_srgba").unwrap(); + + gl.vertex_attrib_pointer_f32( + a_pos_loc, + 2, + glow::FLOAT, + false, + std::mem::size_of::() as i32, + offset_of!(Vertex, pos) as i32, + ); + gl.enable_vertex_attrib_array(a_pos_loc); + + gl.vertex_attrib_pointer_f32( + a_tc_loc, + 2, + glow::FLOAT, + false, + std::mem::size_of::() as i32, + offset_of!(Vertex, uv) as i32, + ); + gl.enable_vertex_attrib_array(a_tc_loc); + + gl.vertex_attrib_pointer_f32( + a_srgba_loc, + 4, + glow::UNSIGNED_BYTE, + false, + std::mem::size_of::() as i32, + offset_of!(Vertex, color) as i32, + ); + gl.enable_vertex_attrib_array(a_srgba_loc); + assert_eq!(gl.get_error(), glow::NO_ERROR, "OpenGL error occurred!"); + + Painter { + program, + u_screen_size, + u_sampler, + egui_texture: None, + egui_texture_version: None, + user_textures: Default::default(), + vertex_array, + vertex_buffer, + element_array_buffer, + old_textures: Vec::new(), + #[cfg(debug_assertions)] + destroyed: false, + } + } + } + + pub fn upload_egui_texture(&mut self, gl: &glow::Context, texture: &egui::Texture) { + self.assert_not_destroyed(); + + if self.egui_texture_version == Some(texture.version) { + return; // No change + } + + let pixels: Vec = texture + .pixels + .iter() + .flat_map(|a| Vec::from(Color32::from_white_alpha(*a).to_array())) + .collect(); + + if let Some(old_tex) = std::mem::replace( + &mut self.egui_texture, + Some(srgbtexture2d(gl, &pixels, texture.width, texture.height)), + ) { + unsafe { + gl.delete_texture(old_tex); + } + } + self.egui_texture_version = Some(texture.version); + } + + unsafe fn prepare_painting( + &mut self, + gl_window: &glutin::WindowedContext, + gl: &glow::Context, + pixels_per_point: f32, + ) -> (u32, u32) { + gl.enable(glow::SCISSOR_TEST); + // egui outputs mesh in both winding orders: + gl.disable(glow::CULL_FACE); + + gl.enable(glow::BLEND); + gl.blend_equation(glow::FUNC_ADD); + gl.blend_func_separate( + // egui outputs colors with premultiplied alpha: + glow::ONE, + glow::ONE_MINUS_SRC_ALPHA, + // Less important, but this is technically the correct alpha blend function + // when you want to make use of the framebuffer alpha (for screenshots, compositing, etc). + glow::ONE_MINUS_DST_ALPHA, + glow::ONE, + ); + + let glutin::dpi::PhysicalSize { + width: width_in_pixels, + height: height_in_pixels, + } = gl_window.window().inner_size(); + let width_in_points = width_in_pixels as f32 / pixels_per_point; + let height_in_points = height_in_pixels as f32 / pixels_per_point; + + gl.viewport(0, 0, width_in_pixels as i32, height_in_pixels as i32); + + gl.use_program(Some(self.program)); + + // The texture coordinates for text are so that both nearest and linear should work with the egui font texture. + // For user textures linear sampling is more likely to be the right choice. + gl.uniform_2_f32(Some(&self.u_screen_size), width_in_points, height_in_points); + gl.uniform_1_i32(Some(&self.u_sampler), 0); + gl.active_texture(glow::TEXTURE0); + + gl.bind_vertex_array(Some(self.vertex_array)); + gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer)); + gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.element_array_buffer)); + + (width_in_pixels, height_in_pixels) + } + + /// Main entry-point for painting a frame. + /// You should call `target.clear_color(..)` before + /// and `target.finish()` after this. + /// + /// The following OpenGL features will be set: + /// - Scissor test will be enabled + /// - Cull face will be disabled + /// - Blend will be enabled + /// + /// The scissor area and blend parameters will be changed. + /// + /// As well as this, the following objects will be rebound: + /// - Vertex Array + /// - Vertex Buffer + /// - Element Buffer + /// - Texture (and active texture will be set to 0) + /// - Program + /// + /// Please be mindful of these effects when integrating into your program, and also be mindful + /// of the effects your program might have on this code. Look at the source if in doubt. + pub fn paint_meshes( + &mut self, + gl_window: &glutin::WindowedContext, + gl: &glow::Context, + pixels_per_point: f32, + clipped_meshes: Vec, + egui_texture: &egui::Texture, + ) { + self.assert_not_destroyed(); + + self.upload_egui_texture(gl, egui_texture); + self.upload_pending_user_textures(gl); + + let size_in_pixels = unsafe { self.prepare_painting(gl_window, gl, pixels_per_point) }; + for egui::ClippedMesh(clip_rect, mesh) in clipped_meshes { + self.paint_mesh(gl, size_in_pixels, pixels_per_point, clip_rect, &mesh) + } + + assert_eq!( + unsafe { gl.get_error() }, + glow::NO_ERROR, + "OpenGL error occurred!" + ); + } + + #[inline(never)] // Easier profiling + fn paint_mesh( + &mut self, + gl: &glow::Context, + size_in_pixels: (u32, u32), + pixels_per_point: f32, + clip_rect: Rect, + mesh: &Mesh, + ) { + debug_assert!(mesh.is_valid()); + + if let Some(texture) = self.get_texture(mesh.texture_id) { + unsafe { + gl.buffer_data_u8_slice( + glow::ARRAY_BUFFER, + as_u8_slice(mesh.vertices.as_slice()), + glow::STREAM_DRAW, + ); + + gl.buffer_data_u8_slice( + glow::ELEMENT_ARRAY_BUFFER, + as_u8_slice(mesh.indices.as_slice()), + glow::STREAM_DRAW, + ); + + gl.bind_texture(glow::TEXTURE_2D, Some(texture)); + } + // Transform clip rect to physical pixels: + let clip_min_x = pixels_per_point * clip_rect.min.x; + let clip_min_y = pixels_per_point * clip_rect.min.y; + let clip_max_x = pixels_per_point * clip_rect.max.x; + let clip_max_y = pixels_per_point * clip_rect.max.y; + + // Make sure clip rect can fit within a `u32`: + let clip_min_x = clip_min_x.clamp(0.0, size_in_pixels.0 as f32); + let clip_min_y = clip_min_y.clamp(0.0, size_in_pixels.1 as f32); + let clip_max_x = clip_max_x.clamp(clip_min_x, size_in_pixels.0 as f32); + let clip_max_y = clip_max_y.clamp(clip_min_y, size_in_pixels.1 as f32); + + let clip_min_x = clip_min_x.round() as i32; + let clip_min_y = clip_min_y.round() as i32; + let clip_max_x = clip_max_x.round() as i32; + let clip_max_y = clip_max_y.round() as i32; + + unsafe { + gl.scissor( + clip_min_x, + size_in_pixels.1 as i32 - clip_max_y, + clip_max_x - clip_min_x, + clip_max_y - clip_min_y, + ); + gl.draw_elements( + glow::TRIANGLES, + mesh.indices.len() as i32, + glow::UNSIGNED_INT, + 0, + ); + } + } + } + + // ------------------------------------------------------------------------ + // user textures: this is an experimental feature. + // No need to implement this in your egui integration! + + pub fn alloc_user_texture(&mut self) -> egui::TextureId { + self.assert_not_destroyed(); + + for (i, tex) in self.user_textures.iter_mut().enumerate() { + if tex.is_none() { + *tex = Some(Default::default()); + return egui::TextureId::User(i as u64); + } + } + let id = egui::TextureId::User(self.user_textures.len() as u64); + self.user_textures.push(Some(Default::default())); + id + } + + /// register glow texture as egui texture + /// Usable for render to image rectangle + pub fn register_glow_texture(&mut self, texture: glow::NativeTexture) -> egui::TextureId { + self.assert_not_destroyed(); + + let id = self.alloc_user_texture(); + if let egui::TextureId::User(id) = id { + if let Some(Some(user_texture)) = self.user_textures.get_mut(id as usize) { + if let UserTexture { + gl_texture: Some(old_tex), + .. + } = std::mem::replace( + user_texture, + UserTexture { + data: vec![], + size: (0, 0), + gl_texture: Some(texture), + }, + ) { + self.old_textures.push(old_tex); + } + } + } + id + } + + pub fn set_user_texture( + &mut self, + id: egui::TextureId, + size: (usize, usize), + pixels: &[Color32], + ) { + self.assert_not_destroyed(); + + assert_eq!( + size.0 * size.1, + pixels.len(), + "Mismatch between texture size and texel count" + ); + + if let egui::TextureId::User(id) = id { + if let Some(Some(user_texture)) = self.user_textures.get_mut(id as usize) { + let data: Vec = pixels + .iter() + .flat_map(|srgba| Vec::from(srgba.to_array())) + .collect(); + + if let UserTexture { + gl_texture: Some(old_tex), + .. + } = std::mem::replace( + user_texture, + UserTexture { + data, + size, + gl_texture: None, + }, + ) { + self.old_textures.push(old_tex); + } + } + } + } + + pub fn free_user_texture(&mut self, id: egui::TextureId) { + self.assert_not_destroyed(); + + if let egui::TextureId::User(id) = id { + let index = id as usize; + if index < self.user_textures.len() { + self.user_textures[index] = None; + } + } + } + + pub fn get_texture(&self, texture_id: egui::TextureId) -> Option { + self.assert_not_destroyed(); + + match texture_id { + egui::TextureId::Egui => self.egui_texture, + egui::TextureId::User(id) => self.user_textures.get(id as usize)?.as_ref()?.gl_texture, + } + } + + pub fn upload_pending_user_textures(&mut self, gl: &glow::Context) { + self.assert_not_destroyed(); + + for user_texture in self.user_textures.iter_mut().flatten() { + if user_texture.gl_texture.is_none() { + let data = std::mem::take(&mut user_texture.data); + user_texture.gl_texture = Some(srgbtexture2d( + gl, + &data, + user_texture.size.0, + user_texture.size.1, + )); + user_texture.size = (0, 0); + } + } + for t in self.old_textures.drain(..) { + unsafe { + gl.delete_texture(t); + } + } + } + + unsafe fn destroy_gl(&self, gl: &glow::Context) { + gl.delete_program(self.program); + if let Some(tex) = self.egui_texture { + gl.delete_texture(tex); + } + for tex in self.user_textures.iter().flatten() { + if let Some(t) = tex.gl_texture { + gl.delete_texture(t); + } + } + gl.delete_vertex_array(self.vertex_array); + gl.delete_buffer(self.vertex_buffer); + gl.delete_buffer(self.element_array_buffer); + for t in &self.old_textures { + gl.delete_texture(*t); + } + } + + /// This function must be called before Painter is dropped, as Painter has some OpenGL objects + /// that should be deleted. + #[cfg(debug_assertions)] + pub fn destroy(&mut self, gl: &glow::Context) { + debug_assert!(!self.destroyed, "Only destroy egui once!"); + unsafe { + self.destroy_gl(gl); + } + self.destroyed = true; + } + + #[cfg(not(debug_assertions))] + pub fn destroy(&self, gl: &glow::Context) { + unsafe { + self.destroy_gl(gl); + } + } + + #[cfg(debug_assertions)] + fn assert_not_destroyed(&self) { + assert!(!self.destroyed, "egui has already been destroyed!"); + } + + #[inline(always)] + #[cfg(not(debug_assertions))] + #[allow(clippy::unused_self)] + fn assert_not_destroyed(&self) {} +} + +impl Drop for Painter { + fn drop(&mut self) { + #[cfg(debug_assertions)] + assert!( + self.destroyed, + "Make sure to destroy() rather than dropping, to avoid leaking OpenGL objects!" + ); + } +} diff --git a/egui_glow/src/persistence.rs b/egui_glow/src/persistence.rs new file mode 100644 index 000000000000..97df78e08d35 --- /dev/null +++ b/egui_glow/src/persistence.rs @@ -0,0 +1,95 @@ +use std::{ + collections::HashMap, + path::{Path, PathBuf}, +}; + +// ---------------------------------------------------------------------------- + +/// A key-value store backed by a [RON](https://github.com/ron-rs/ron) file on disk. +/// Used to restore egui state, native window position/size and app state. +pub struct FileStorage { + path: PathBuf, + kv: HashMap, + dirty: bool, +} + +impl FileStorage { + pub fn from_path(path: impl Into) -> Self { + let path: PathBuf = path.into(); + Self { + kv: read_ron(&path).unwrap_or_default(), + path, + dirty: false, + } + } +} + +impl epi::Storage for FileStorage { + fn get_string(&self, key: &str) -> Option { + self.kv.get(key).cloned() + } + + fn set_string(&mut self, key: &str, value: String) { + if self.kv.get(key) != Some(&value) { + self.kv.insert(key.to_owned(), value); + self.dirty = true; + } + } + + fn flush(&mut self) { + if self.dirty { + // eprintln!("Persisted to {}", self.path.display()); + let file = std::fs::File::create(&self.path).unwrap(); + let config = Default::default(); + ron::ser::to_writer_pretty(file, &self.kv, config).unwrap(); + self.dirty = false; + } + } +} + +// ---------------------------------------------------------------------------- + +pub fn read_ron(ron_path: impl AsRef) -> Option +where + T: serde::de::DeserializeOwned, +{ + match std::fs::File::open(ron_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + match ron::de::from_reader(reader) { + Ok(value) => Some(value), + Err(err) => { + eprintln!("ERROR: Failed to parse RON: {}", err); + None + } + } + } + Err(_err) => { + // File probably doesn't exist. That's fine. + None + } + } +} +// ---------------------------------------------------------------------------- + +/// Alternative to `FileStorage` +pub fn read_memory(ctx: &egui::Context, memory_file_path: impl AsRef) { + let memory: Option = read_ron(memory_file_path); + if let Some(memory) = memory { + *ctx.memory() = memory; + } +} + +/// Alternative to `FileStorage` +/// +/// # Errors +/// When failing to serialize or create the file. +pub fn write_memory( + ctx: &egui::Context, + memory_file_path: impl AsRef, +) -> Result<(), Box> { + let file = std::fs::File::create(memory_file_path)?; + let ron_config = Default::default(); + ron::ser::to_writer_pretty(file, &*ctx.memory(), ron_config)?; + Ok(()) +} diff --git a/egui_glow/src/shader/fragment.glsl b/egui_glow/src/shader/fragment.glsl new file mode 100644 index 000000000000..e7c3c95d2973 --- /dev/null +++ b/egui_glow/src/shader/fragment.glsl @@ -0,0 +1,73 @@ +#ifdef GL_ES +precision mediump float; +#endif + +uniform sampler2D u_sampler; +#if defined(GL_ES) || __VERSION__ < 140 +varying vec4 v_rgba; +varying vec2 v_tc; +#else +in vec4 v_rgba; +in vec2 v_tc; +out vec4 f_color; +#endif + +#ifdef GL_ES +// 0-255 sRGB from 0-1 linear +vec3 srgb_from_linear(vec3 rgb) { + bvec3 cutoff = lessThan(rgb, vec3(0.0031308)); + vec3 lower = rgb * vec3(3294.6); + vec3 higher = vec3(269.025) * pow(rgb, vec3(1.0 / 2.4)) - vec3(14.025); + return mix(higher, lower, vec3(cutoff)); +} + +vec4 srgba_from_linear(vec4 rgba) { + return vec4(srgb_from_linear(rgba.rgb), 255.0 * rgba.a); +} + +#if __VERSION__ < 300 +// 0-1 linear from 0-255 sRGB +vec3 linear_from_srgb(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(10.31475)); + vec3 lower = srgb / vec3(3294.6); + vec3 higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); + return mix(higher, lower, vec3(cutoff)); +} + +vec4 linear_from_srgba(vec4 srgba) { + return vec4(linear_from_srgb(srgba.rgb), srgba.a / 255.0); +} +#endif +#endif + +#ifdef GL_ES +void main() { +#if __VERSION__ < 300 + // We must decode the colors, since WebGL doesn't come with sRGBA textures: + vec4 texture_rgba = linear_from_srgba(texture2D(u_sampler, v_tc) * 255.0); +#else + // The texture is set up with `SRGB8_ALPHA8`, so no need to decode here! + vec4 texture_rgba = texture2D(u_sampler, v_tc); +#endif + + /// Multiply vertex color with texture color (in linear space). + gl_FragColor = v_rgba * texture_rgba; + + // We must gamma-encode again since WebGL doesn't support linear blending in the framebuffer. + gl_FragColor = srgba_from_linear(v_rgba * texture_rgba) / 255.0; + + // WebGL doesn't support linear blending in the framebuffer, + // so we apply this hack to at least get a bit closer to the desired blending: + gl_FragColor.a = pow(gl_FragColor.a, 1.6); // Empiric nonsense +} +#else +void main() { + // The texture sampler is sRGB aware, and OpenGL already expects linear rgba output + // so no need for any sRGB conversions here: +#if __VERSION__ < 140 + gl_FragColor = v_rgba * texture2D(u_sampler, v_tc); +#else + f_color = v_rgba * texture(u_sampler, v_tc); +#endif +} +#endif diff --git a/egui_glow/src/shader/vertex.glsl b/egui_glow/src/shader/vertex.glsl new file mode 100644 index 000000000000..cf2309a8376d --- /dev/null +++ b/egui_glow/src/shader/vertex.glsl @@ -0,0 +1,42 @@ +#if !defined(GL_ES) && __VERSION__ >= 140 +#define I in +#define O out +#define V(x) x +#else +#define I attribute +#define O varying +#define V(x) vec3(x) +#endif + +#ifdef GL_ES +precision mediump float; +#endif +uniform vec2 u_screen_size; +I vec2 a_pos; +I vec4 a_srgba; // 0-255 sRGB +I vec2 a_tc; +O vec4 v_rgba; +O vec2 v_tc; + +// 0-1 linear from 0-255 sRGB +vec3 linear_from_srgb(vec3 srgb) { + bvec3 cutoff = lessThan(srgb, vec3(10.31475)); + vec3 lower = srgb / vec3(3294.6); + vec3 higher = pow((srgb + vec3(14.025)) / vec3(269.025), vec3(2.4)); + return mix(higher, lower, V(cutoff)); +} + +vec4 linear_from_srgba(vec4 srgba) { + return vec4(linear_from_srgb(srgba.rgb), srgba.a / 255.0); +} + +void main() { + gl_Position = vec4( + 2.0 * a_pos.x / u_screen_size.x - 1.0, + 1.0 - 2.0 * a_pos.y / u_screen_size.y, + 0.0, + 1.0); + // egui encodes vertex colors in gamma spaces, so we must decode the colors here: + v_rgba = linear_from_srgba(a_srgba); + v_tc = a_tc; +}