Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Shape::Callback to do custom rendering inside of an egui UI #1351

Merged
merged 7 commits into from Mar 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 4 additions & 2 deletions CHANGELOG.md
Expand Up @@ -7,12 +7,14 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
## Unreleased

### Added ⭐
* Added `Frame::canvas` ([1362](https://github.com/emilk/egui/pull/1362)).
* Add `Shape::Callback` for backend-specific painting ([#1351](https://github.com/emilk/egui/pull/1351)).
* Added `Frame::canvas` ([#1362](https://github.com/emilk/egui/pull/1362)).

### Changed 🔧
* `ClippedMesh` has been replaced with `ClippedPrimitive` ([#1351](https://github.com/emilk/egui/pull/1351)).

### Fixed 🐛
* Fixed ComboBoxes always being rendered left-aligned ([1304](https://github.com/emilk/egui/pull/1304)).
* Fixed ComboBoxes always being rendered left-aligned ([#1304](https://github.com/emilk/egui/pull/1304)).


## 0.17.0 - 2022-02-22 - Improved font selection and image handling
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -204,9 +204,9 @@ loop {
let full_output = egui_ctx.run(raw_input, |egui_ctx| {
my_app.ui(egui_ctx); // add panels, windows and widgets to `egui_ctx` here
});
let clipped_meshes = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint
let clipped_primitives = egui_ctx.tessellate(full_output.shapes); // creates triangles to paint

my_integration.paint(&full_output.textures_delta, clipped_meshes);
my_integration.paint(&full_output.textures_delta, clipped_primitives);

let platform_output = full_output.platform_output;
my_integration.set_cursor_icon(platform_output.cursor_icon);
Expand Down
2 changes: 2 additions & 0 deletions eframe/Cargo.toml
Expand Up @@ -66,9 +66,11 @@ egui_web = { version = "0.17.0", path = "../egui_web", default-features = false
# For examples:
egui_extras = { path = "../egui_extras", features = ["image", "svg"] }
ehttp = "0.2"
glow = "0.11"
image = { version = "0.24", default-features = false, features = [
"jpeg",
"png",
] }
parking_lot = "0.12"
poll-promise = "0.1"
rfd = "0.8"
193 changes: 193 additions & 0 deletions eframe/examples/custom_3d.rs
@@ -0,0 +1,193 @@
//! This demo shows how to embed 3D rendering using [`glow`](https://github.com/grovesNL/glow) in `eframe`.
//!
//! This is very advanced usage, and you need to be careful.
//!
//! If you want an easier way to show 3D graphics with egui, take a look at:
//! * [`bevy_egui`](https://github.com/mvlabat/bevy_egui)
//! * [`three-d`](https://github.com/asny/three-d)

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release

use eframe::{egui, epi};

use parking_lot::Mutex;
use std::sync::Arc;

#[derive(Default)]
struct MyApp {
rotating_triangle: Arc<Mutex<Option<RotatingTriangle>>>,
angle: f32,
}

impl epi::App for MyApp {
fn name(&self) -> &str {
"Custom 3D painting inside an egui window"
}

fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Here is some 3D stuff:");

egui::ScrollArea::both().show(ui, |ui| {
egui::Frame::dark_canvas(ui.style()).show(ui, |ui| {
self.custom_painting(ui);
});
ui.label("Drag to rotate!");
});
});

let mut frame = egui::Frame::window(&*ctx.style());
frame.fill = frame.fill.linear_multiply(0.5); // transparent
egui::Window::new("3D stuff in a window")
.frame(frame)
.show(ctx, |ui| {
self.custom_painting(ui);
});
}
}

impl MyApp {
fn custom_painting(&mut self, ui: &mut egui::Ui) {
let (rect, response) =
ui.allocate_exact_size(egui::Vec2::splat(256.0), egui::Sense::drag());

self.angle += response.drag_delta().x * 0.01;

let angle = self.angle;
let rotating_triangle = self.rotating_triangle.clone();

let callback = egui::epaint::PaintCallback {
rect,
callback: std::sync::Arc::new(move |render_ctx| {
if let Some(painter) = render_ctx.downcast_ref::<egui_glow::Painter>() {
let mut rotating_triangle = rotating_triangle.lock();
let rotating_triangle = rotating_triangle
.get_or_insert_with(|| RotatingTriangle::new(painter.gl()));
rotating_triangle.paint(painter.gl(), angle);
} else {
eprintln!("Can't do custom painting because we are not using a glow context");
}
}),
};
ui.painter().add(callback);
}
}

struct RotatingTriangle {
program: glow::Program,
vertex_array: glow::VertexArray,
}

impl RotatingTriangle {
fn new(gl: &glow::Context) -> Self {
use glow::HasContext as _;

let shader_version = if cfg!(target_arch = "wasm32") {
"#version 300 es"
} else {
"#version 410"
};

unsafe {
let program = gl.create_program().expect("Cannot create program");

let (vertex_shader_source, fragment_shader_source) = (
r#"
const vec2 verts[3] = vec2[3](
vec2(0.0, 1.0),
vec2(-1.0, -1.0),
vec2(1.0, -1.0)
);
const vec4 colors[3] = vec4[3](
vec4(1.0, 0.0, 0.0, 1.0),
vec4(0.0, 1.0, 0.0, 1.0),
vec4(0.0, 0.0, 1.0, 1.0)
);
out vec4 v_color;
uniform float u_angle;
void main() {
v_color = colors[gl_VertexID];
gl_Position = vec4(verts[gl_VertexID], 0.0, 1.0);
gl_Position.x *= cos(u_angle);
}
"#,
r#"
precision mediump float;
in vec4 v_color;
out vec4 out_color;
void main() {
out_color = v_color;
}
"#,
);

let shader_sources = [
(glow::VERTEX_SHADER, vertex_shader_source),
(glow::FRAGMENT_SHADER, fragment_shader_source),
];

let shaders: Vec<_> = shader_sources
.iter()
.map(|(shader_type, shader_source)| {
let shader = gl
.create_shader(*shader_type)
.expect("Cannot create shader");
gl.shader_source(shader, &format!("{}\n{}", shader_version, shader_source));
gl.compile_shader(shader);
if !gl.get_shader_compile_status(shader) {
panic!("{}", gl.get_shader_info_log(shader));
}
gl.attach_shader(program, shader);
shader
})
.collect();

gl.link_program(program);
if !gl.get_program_link_status(program) {
panic!("{}", gl.get_program_info_log(program));
}

for shader in shaders {
gl.detach_shader(program, shader);
gl.delete_shader(shader);
}

let vertex_array = gl
.create_vertex_array()
.expect("Cannot create vertex array");

Self {
program,
vertex_array,
}
}
}

// TODO: figure out how to call this in a nice way
#[allow(unused)]
fn destroy(self, gl: &glow::Context) {
use glow::HasContext as _;
unsafe {
gl.delete_program(self.program);
gl.delete_vertex_array(self.vertex_array);
}
}

fn paint(&self, gl: &glow::Context, angle: f32) {
use glow::HasContext as _;
unsafe {
gl.use_program(Some(self.program));
gl.uniform_1_f32(
gl.get_uniform_location(self.program, "u_angle").as_ref(),
angle,
);
gl.bind_vertex_array(Some(self.vertex_array));
gl.draw_arrays(glow::TRIANGLES, 0, 3);
}
}
}

fn main() {
let options = eframe::NativeOptions::default();
eframe::run_native(Box::new(MyApp::default()), options);
}
1 change: 1 addition & 0 deletions eframe/examples/custom_font.rs
Expand Up @@ -22,6 +22,7 @@ impl epi::App for MyApp {
ctx: &egui::Context,
_frame: &epi::Frame,
_storage: Option<&dyn epi::Storage>,
_gl: &std::rc::Rc<epi::glow::Context>,
) {
// Start with the default fonts (we will be adding to them rather than replacing them).
let mut fonts = egui::FontDefinitions::default();
Expand Down
10 changes: 7 additions & 3 deletions egui-winit/Cargo.toml
Expand Up @@ -24,6 +24,12 @@ default = ["clipboard", "dark-light", "links"]
# if disabled a clipboard will be simulated so you can still copy/paste within the egui app.
clipboard = ["copypasta"]

# implement bytemuck on most types.
convert_bytemuck = ["egui/convert_bytemuck"]

# Only for `egui_glow` - the official eframe/epi backend.
epi_backend = ["epi", "glow"]

# enable opening links in a browser when an egui hyperlink is clicked.
links = ["webbrowser"]

Expand All @@ -36,9 +42,6 @@ persistence = [
] # can't add epi/persistence here because of https://github.com/rust-lang/cargo/issues/8832
serialize = ["egui/serialize", "serde"]

# implement bytemuck on most types.
convert_bytemuck = ["egui/convert_bytemuck"]


[dependencies]
egui = { version = "0.17.0", path = "../egui", default-features = false, features = [
Expand All @@ -53,6 +56,7 @@ epi = { version = "0.17.0", path = "../epi", optional = true }

copypasta = { version = "0.7", optional = true }
dark-light = { version = "0.2.1", optional = true } # detect dark mode system preference
glow = { version = "0.11", optional = true }
serde = { version = "1.0", optional = true, features = ["derive"] }
webbrowser = { version = "0.6", optional = true }

Expand Down
7 changes: 4 additions & 3 deletions egui-winit/src/epi.rs
Expand Up @@ -232,6 +232,7 @@ impl EpiIntegration {
integration_name: &'static str,
max_texture_side: usize,
window: &winit::window::Window,
gl: &std::rc::Rc<glow::Context>,
repaint_signal: std::sync::Arc<dyn epi::backend::RepaintSignal>,
persistence: crate::epi::Persistence,
app: Box<dyn epi::App>,
Expand Down Expand Up @@ -271,17 +272,17 @@ impl EpiIntegration {
can_drag_window: false,
};

slf.setup(window);
slf.setup(window, gl);
if slf.app.warm_up_enabled() {
slf.warm_up(window);
}

slf
}

fn setup(&mut self, window: &winit::window::Window) {
fn setup(&mut self, window: &winit::window::Window, gl: &std::rc::Rc<glow::Context>) {
self.app
.setup(&self.egui_ctx, &self.frame, self.persistence.storage());
.setup(&self.egui_ctx, &self.frame, self.persistence.storage(), gl);
let app_output = self.frame.take_app_output();

if app_output.quit {
Expand Down
21 changes: 14 additions & 7 deletions egui/src/context.rs
Expand Up @@ -122,7 +122,7 @@ impl ContextImpl {
///
/// ``` no_run
/// # fn handle_platform_output(_: egui::PlatformOutput) {}
/// # fn paint(textures_detla: egui::TexturesDelta, _: Vec<egui::ClippedMesh>) {}
/// # fn paint(textures_detla: egui::TexturesDelta, _: Vec<egui::ClippedPrimitive>) {}
/// let mut ctx = egui::Context::default();
///
/// // Game loop:
Expand All @@ -137,8 +137,8 @@ impl ContextImpl {
/// });
/// });
/// handle_platform_output(full_output.platform_output);
/// let clipped_meshes = ctx.tessellate(full_output.shapes); // create triangles to paint
/// paint(full_output.textures_delta, clipped_meshes);
/// let clipped_primitives = ctx.tessellate(full_output.shapes); // create triangles to paint
/// paint(full_output.textures_delta, clipped_primitives);
/// }
/// ```
#[derive(Clone)]
Expand Down Expand Up @@ -773,7 +773,7 @@ impl Context {
}

/// Tessellate the given shapes into triangle meshes.
pub fn tessellate(&self, shapes: Vec<ClippedShape>) -> Vec<ClippedMesh> {
pub fn tessellate(&self, shapes: Vec<ClippedShape>) -> Vec<ClippedPrimitive> {
// A tempting optimization is to reuse the tessellation from last frame if the
// shapes are the same, but just comparing the shapes takes about 50% of the time
// it takes to tessellate them, so it is not a worth optimization.
Expand All @@ -782,13 +782,13 @@ impl Context {
tessellation_options.pixels_per_point = self.pixels_per_point();
tessellation_options.aa_size = 1.0 / self.pixels_per_point();
let paint_stats = PaintStats::from_shapes(&shapes);
let clipped_meshes = tessellator::tessellate_shapes(
let clipped_primitives = tessellator::tessellate_shapes(
shapes,
tessellation_options,
self.fonts().font_image_size(),
);
self.write().paint_stats = paint_stats.with_clipped_meshes(&clipped_meshes);
clipped_meshes
self.write().paint_stats = paint_stats.with_clipped_primitives(&clipped_primitives);
clipped_primitives
}

// ---------------------------------------------------------------------
Expand Down Expand Up @@ -1246,3 +1246,10 @@ impl Context {
self.set_style(style);
}
}

#[cfg(test)]
#[test]
fn context_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Context>();
}