Add egui_glow backend as alternative to egui_glium (#685)

This commit is contained in:
AlexApps99 2021-10-19 10:13:32 +13:00 committed by GitHub
parent df3aeab434
commit 877e89f2ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 1685 additions and 3 deletions

View file

@ -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.

View file

@ -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

View file

@ -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
View 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
View 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
View file

@ -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).

130
egui_glow/examples/pure.rs Normal file
View 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
View 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
View 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
View 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!"
);
}
}

View 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(())
}

View 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

View 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;
}