Add egui_glow backend as alternative to egui_glium (#685)
This commit is contained in:
parent
df3aeab434
commit
877e89f2ec
13 changed files with 1685 additions and 3 deletions
|
@ -36,6 +36,10 @@ Puts an egui app inside the web browser by compiling to WASM and binding to the
|
||||||
### `egui_glium`
|
### `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).
|
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`
|
### `eframe`
|
||||||
A wrapper around `egui_web` + `egui_glium`, so you can compile the same app for either web or native.
|
A wrapper around `egui_web` + `egui_glium`, so you can compile the same app for either web or native.
|
||||||
|
|
||||||
|
|
|
@ -163,11 +163,12 @@ The integration needs to do two things:
|
||||||
|
|
||||||
### Official
|
### 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_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_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`).
|
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
|
### Debugging your integration
|
||||||
|
|
||||||
|
|
|
@ -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!).
|
* Remove "http" feature (use https://github.com/emilk/ehttp instead!).
|
||||||
* Increase native scroll speed.
|
* Increase native scroll speed.
|
||||||
* Add `App::persist_native_window` and `App::persist_egui_memory` to control what gets persisted.
|
* 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
|
## 0.14.0 - 2021-08-24
|
||||||
|
|
8
egui_glow/CHANGELOG.md
Normal file
8
egui_glow/CHANGELOG.md
Normal file
|
@ -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.
|
70
egui_glow/Cargo.toml
Normal file
70
egui_glow/Cargo.toml
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
[package]
|
||||||
|
name = "egui_glow"
|
||||||
|
version = "0.14.0"
|
||||||
|
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
|
||||||
|
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"]
|
16
egui_glow/README.md
Normal file
16
egui_glow/README.md
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# egui_glow
|
||||||
|
|
||||||
|
[](https://crates.io/crates/egui_glow)
|
||||||
|
[](https://docs.rs/egui_glow)
|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
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).
|
130
egui_glow/examples/pure.rs
Normal file
130
egui_glow/examples/pure.rs
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
//! Example how to use pure `egui_glow` without [`epi`].
|
||||||
|
|
||||||
|
fn create_display(
|
||||||
|
event_loop: &glutin::event_loop::EventLoop<()>,
|
||||||
|
) -> (
|
||||||
|
glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
394
egui_glow/src/backend.rs
Normal file
394
egui_glow/src/backend.rs
Normal file
|
@ -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<Box<dyn epi::Storage>>) -> Option<WindowSettings> {
|
||||||
|
epi::get_value(&**storage.as_ref()?, WINDOW_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "persistence"))]
|
||||||
|
fn deserialize_window_settings(_: &Option<Box<dyn epi::Storage>>) -> Option<WindowSettings> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "persistence")]
|
||||||
|
fn deserialize_memory(storage: &Option<Box<dyn epi::Storage>>) -> Option<egui::Memory> {
|
||||||
|
epi::get_value(&**storage.as_ref()?, EGUI_MEMORY_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "persistence"))]
|
||||||
|
fn deserialize_memory(_: &Option<Box<dyn epi::Storage>>) -> Option<egui::Memory> {
|
||||||
|
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<glutin::event_loop::EventLoopProxy<RequestRepaintEvent>>);
|
||||||
|
|
||||||
|
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<WindowSettings>,
|
||||||
|
window_icon: Option<glutin::window::Icon>,
|
||||||
|
event_loop: &glutin::event_loop::EventLoop<RequestRepaintEvent>,
|
||||||
|
) -> (
|
||||||
|
glutin::WindowedContext<glutin::PossiblyCurrent>,
|
||||||
|
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<Box<dyn epi::Storage>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "persistence")]
|
||||||
|
fn create_storage(app_name: &str) -> Option<Box<dyn epi::Storage>> {
|
||||||
|
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<f32>,
|
||||||
|
) -> 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> {
|
||||||
|
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<dyn epi::App>, 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::<f32>(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();
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
226
egui_glow/src/lib.rs
Normal file
226
egui_glow/src/lib.rs
Normal file
|
@ -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<f64> {
|
||||||
|
#[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<glutin::PossiblyCurrent>,
|
||||||
|
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<egui::epaint::ClippedShape>) {
|
||||||
|
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<glutin::PossiblyCurrent>,
|
||||||
|
gl: &glow::Context,
|
||||||
|
shapes: Vec<egui::epaint::ClippedShape>,
|
||||||
|
) {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
622
egui_glow/src/painter.rs
Normal file
622
egui_glow/src/painter.rs
Normal file
|
@ -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<T>(s: &[T]) -> &[u8] {
|
||||||
|
std::slice::from_raw_parts(s.as_ptr().cast::<u8>(), s.len() * std::mem::size_of::<T>())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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<glow::NativeTexture>,
|
||||||
|
egui_texture_version: Option<u64>,
|
||||||
|
|
||||||
|
/// `None` means unallocated (freed) slot.
|
||||||
|
user_textures: Vec<Option<UserTexture>>,
|
||||||
|
|
||||||
|
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<glow::NativeTexture>,
|
||||||
|
// 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<u8>,
|
||||||
|
size: (usize, usize),
|
||||||
|
|
||||||
|
/// Lazily uploaded
|
||||||
|
gl_texture: Option<glow::NativeTexture>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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::<Vec<u8>>()
|
||||||
|
.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::<Vertex>() 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::<Vertex>() 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::<Vertex>() 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<u8> = 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<glutin::PossiblyCurrent>,
|
||||||
|
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<glutin::PossiblyCurrent>,
|
||||||
|
gl: &glow::Context,
|
||||||
|
pixels_per_point: f32,
|
||||||
|
clipped_meshes: Vec<egui::ClippedMesh>,
|
||||||
|
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<u8> = 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<glow::NativeTexture> {
|
||||||
|
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!"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
95
egui_glow/src/persistence.rs
Normal file
95
egui_glow/src/persistence.rs
Normal file
|
@ -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<String, String>,
|
||||||
|
dirty: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileStorage {
|
||||||
|
pub fn from_path(path: impl Into<PathBuf>) -> 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<String> {
|
||||||
|
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<T>(ron_path: impl AsRef<Path>) -> Option<T>
|
||||||
|
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<std::path::Path>) {
|
||||||
|
let memory: Option<egui::Memory> = 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<std::path::Path>,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
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(())
|
||||||
|
}
|
73
egui_glow/src/shader/fragment.glsl
Normal file
73
egui_glow/src/shader/fragment.glsl
Normal file
|
@ -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
|
42
egui_glow/src/shader/vertex.glsl
Normal file
42
egui_glow/src/shader/vertex.glsl
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in a new issue