1
0
mirror of https://github.com/emilk/egui.git synced 2026-06-28 07:23:13 -04:00

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

* Add Shape::Callback to do custom rendering inside of an egui UI
* Use Rc<glow::Context> everywhere
* Remove trait WebPainter
* Add glow::Context to epi::App::setup
This commit is contained in:
Emil Ernerfeldt
2022-03-14 13:25:11 +01:00
committed by GitHub
parent 002158050b
commit 6aee4997d4
34 changed files with 777 additions and 385 deletions

View File

@@ -110,7 +110,7 @@ pub use {
image::{AlphaImage, ColorImage, ImageData, ImageDelta},
mesh::{Mesh, Mesh16, Vertex},
shadow::Shadow,
shape::{CircleShape, PathShape, RectShape, Rounding, Shape, TextShape},
shape::{CircleShape, PaintCallback, PathShape, RectShape, Rounding, Shape, TextShape},
stats::PaintStats,
stroke::Stroke,
tessellator::{tessellate_shapes, TessellationOptions, Tessellator},
@@ -166,18 +166,24 @@ pub struct ClippedShape(
pub Shape,
);
/// A [`Mesh`] within a clip rectangle.
/// A [`Mesh`] or [`PaintCallback`] within a clip rectangle.
///
/// Everything is using logical points.
#[derive(Clone, Debug)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ClippedMesh(
pub struct ClippedPrimitive {
/// Clip / scissor rectangle.
/// Only show the part of the [`Mesh`] that falls within this.
pub emath::Rect,
/// The shape
pub Mesh,
);
pub clip_rect: emath::Rect,
/// What to paint - either a [`Mesh`] or a [`PaintCallback`].
pub primitive: Primitive,
}
/// A rendering primitive - either a [`Mesh`] or a [`PaintCallback`].
#[derive(Clone, Debug)]
pub enum Primitive {
Mesh(Mesh),
Callback(PaintCallback),
}
// ----------------------------------------------------------------------------

View File

@@ -1,10 +1,13 @@
//! The different shapes that can be painted.
use crate::{
text::{FontId, Fonts, Galley},
Color32, Mesh, Stroke,
};
use crate::{CubicBezierShape, QuadraticBezierShape};
use emath::*;
pub use crate::{CubicBezierShape, QuadraticBezierShape};
/// A paint primitive such as a circle or a piece of text.
/// Coordinates are all screen space points (not physical pixels).
#[must_use = "Add a Shape to a Painter"]
@@ -29,6 +32,16 @@ pub enum Shape {
Mesh(Mesh),
QuadraticBezier(QuadraticBezierShape),
CubicBezier(CubicBezierShape),
/// Backend-specific painting.
Callback(PaintCallback),
}
#[cfg(test)]
#[test]
fn shape_impl_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Shape>();
}
impl From<Vec<Shape>> for Shape {
@@ -196,6 +209,7 @@ impl Shape {
Self::Mesh(mesh) => mesh.calc_bounds(),
Self::QuadraticBezier(bezier) => bezier.visual_bounding_rect(),
Self::CubicBezier(bezier) => bezier.visual_bounding_rect(),
Self::Callback(custom) => custom.rect,
}
}
}
@@ -252,6 +266,9 @@ impl Shape {
*p += delta;
}
}
Shape::Callback(shape) => {
shape.rect = shape.rect.translate(delta);
}
}
}
}
@@ -616,3 +633,57 @@ fn dashes_from_line(
position_on_segment -= segment_length;
});
}
// ----------------------------------------------------------------------------
/// If you want to paint some 3D shapes inside an egui region, you can use this.
///
/// This is advanced usage, and is backend specific.
#[derive(Clone)]
pub struct PaintCallback {
/// Where to paint.
pub rect: Rect,
/// Paint something custom using.
///
/// The argument is the render context, and what it contains depends on the backend.
/// In `eframe` it will be `egui_glow::Painter`.
///
/// The rendering backend is responsible for first setting the active viewport to [`Self::rect`].
/// The rendering backend is also responsible for restoring any state it needs,
/// such as the bound shader program and vertex array.
pub callback: std::sync::Arc<dyn Fn(&dyn std::any::Any) + Send + Sync>,
}
impl PaintCallback {
#[inline]
pub fn call(&self, render_ctx: &dyn std::any::Any) {
(self.callback)(render_ctx);
}
}
impl std::fmt::Debug for PaintCallback {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CustomShape")
.field("rect", &self.rect)
.finish_non_exhaustive()
}
}
impl std::cmp::PartialEq for PaintCallback {
fn eq(&self, other: &Self) -> bool {
// As I understand it, the problem this clippy is trying to protect against
// can only happen if we do dynamic casts back and forth on the pointers, and we don't do that.
#[allow(clippy::vtable_address_comparisons)]
{
self.rect.eq(&other.rect) && std::sync::Arc::ptr_eq(&self.callback, &other.callback)
}
}
}
impl From<PaintCallback> for Shape {
#[inline(always)]
fn from(shape: PaintCallback) -> Self {
Self::Callback(shape)
}
}

View File

@@ -51,5 +51,8 @@ pub fn adjust_colors(shape: &mut Shape, adjust_color: &impl Fn(&mut Color32)) {
adjust_color(&mut bezier.fill);
adjust_color(&mut bezier.stroke.color);
}
Shape::Callback(_) => {
// Can't tint user callback code
}
}
}

View File

@@ -162,12 +162,13 @@ pub struct PaintStats {
pub shape_path: AllocInfo,
pub shape_mesh: AllocInfo,
pub shape_vec: AllocInfo,
pub num_callbacks: usize,
pub text_shape_vertices: AllocInfo,
pub text_shape_indices: AllocInfo,
/// Number of separate clip rectangles
pub clipped_meshes: AllocInfo,
pub clipped_primitives: AllocInfo,
pub vertices: AllocInfo,
pub indices: AllocInfo,
}
@@ -215,27 +216,25 @@ impl PaintStats {
Shape::Mesh(mesh) => {
self.shape_mesh += AllocInfo::from_mesh(mesh);
}
Shape::Callback(_) => {
self.num_callbacks += 1;
}
}
}
pub fn with_clipped_meshes(mut self, clipped_meshes: &[crate::ClippedMesh]) -> Self {
self.clipped_meshes += AllocInfo::from_slice(clipped_meshes);
for ClippedMesh(_, indices) in clipped_meshes {
self.vertices += AllocInfo::from_slice(&indices.vertices);
self.indices += AllocInfo::from_slice(&indices.indices);
pub fn with_clipped_primitives(
mut self,
clipped_primitives: &[crate::ClippedPrimitive],
) -> Self {
self.clipped_primitives += AllocInfo::from_slice(clipped_primitives);
for clipped_primitive in clipped_primitives {
if let Primitive::Mesh(mesh) = &clipped_primitive.primitive {
self.vertices += AllocInfo::from_slice(&mesh.vertices);
self.indices += AllocInfo::from_slice(&mesh.indices);
}
}
self
}
// pub fn total(&self) -> AllocInfo {
// self.shapes
// + self.shape_text
// + self.shape_path
// + self.shape_mesh
// + self.clipped_meshes
// + self.vertices
// + self.indices
// }
}
fn megabytes(size: usize) -> String {

View File

@@ -781,6 +781,9 @@ impl Tessellator {
self.tessellate_quadratic_bezier(quadratic_shape, out);
}
Shape::CubicBezier(cubic_shape) => self.tessellate_cubic_bezier(cubic_shape, out),
Shape::Callback(_) => {
panic!("Shape::Callback passed to Tessellator");
}
}
}
@@ -1046,58 +1049,97 @@ pub fn tessellate_shapes(
shapes: Vec<ClippedShape>,
options: TessellationOptions,
tex_size: [usize; 2],
) -> Vec<ClippedMesh> {
) -> Vec<ClippedPrimitive> {
let mut tessellator = Tessellator::from_options(options);
let mut clipped_meshes: Vec<ClippedMesh> = Vec::default();
let mut clipped_primitives: Vec<ClippedPrimitive> = Vec::default();
for ClippedShape(clip_rect, shape) in shapes {
if !clip_rect.is_positive() {
for ClippedShape(new_clip_rect, new_shape) in shapes {
if !new_clip_rect.is_positive() {
continue; // skip empty clip rectangles
}
let start_new_mesh = match clipped_meshes.last() {
None => true,
Some(cm) => cm.0 != clip_rect || cm.1.texture_id != shape.texture_id(),
};
if let Shape::Callback(callback) = new_shape {
clipped_primitives.push(ClippedPrimitive {
clip_rect: new_clip_rect,
primitive: Primitive::Callback(callback),
});
} else {
let start_new_mesh = match clipped_primitives.last() {
None => true,
Some(output_clipped_primitive) => {
output_clipped_primitive.clip_rect != new_clip_rect
|| if let Primitive::Mesh(output_mesh) = &output_clipped_primitive.primitive
{
output_mesh.texture_id != new_shape.texture_id()
} else {
true
}
}
};
if start_new_mesh {
clipped_meshes.push(ClippedMesh(clip_rect, Mesh::default()));
}
if start_new_mesh {
clipped_primitives.push(ClippedPrimitive {
clip_rect: new_clip_rect,
primitive: Primitive::Mesh(Mesh::default()),
});
}
let out = &mut clipped_meshes.last_mut().unwrap().1;
tessellator.clip_rect = clip_rect;
tessellator.tessellate_shape(tex_size, shape, out);
}
let out = clipped_primitives.last_mut().unwrap();
if options.debug_paint_clip_rects {
for ClippedMesh(clip_rect, mesh) in &mut clipped_meshes {
if mesh.texture_id == TextureId::default() {
tessellator.clip_rect = Rect::EVERYTHING;
tessellator.tessellate_shape(
tex_size,
Shape::rect_stroke(
*clip_rect,
0.0,
Stroke::new(2.0, Color32::from_rgb(150, 255, 150)),
),
mesh,
);
if let Primitive::Mesh(out_mesh) = &mut out.primitive {
tessellator.clip_rect = new_clip_rect;
tessellator.tessellate_shape(tex_size, new_shape, out_mesh);
} else {
// TODO: create a new `ClippedMesh` just for the painted clip rectangle
unreachable!();
}
}
}
if options.debug_paint_clip_rects {
clipped_primitives = add_clip_rects(&mut tessellator, tex_size, clipped_primitives);
}
if options.debug_ignore_clip_rects {
for ClippedMesh(clip_rect, _) in &mut clipped_meshes {
*clip_rect = Rect::EVERYTHING;
for clipped_primitive in &mut clipped_primitives {
clipped_primitive.clip_rect = Rect::EVERYTHING;
}
}
for ClippedMesh(_, mesh) in &clipped_meshes {
crate::epaint_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh");
for clipped_primitive in &clipped_primitives {
if let Primitive::Mesh(mesh) = &clipped_primitive.primitive {
crate::epaint_assert!(mesh.is_valid(), "Tessellator generated invalid Mesh");
}
}
clipped_meshes
clipped_primitives
}
fn add_clip_rects(
tessellator: &mut Tessellator,
tex_size: [usize; 2],
clipped_primitives: Vec<ClippedPrimitive>,
) -> Vec<ClippedPrimitive> {
tessellator.clip_rect = Rect::EVERYTHING;
let stroke = Stroke::new(2.0, Color32::from_rgb(150, 255, 150));
clipped_primitives
.into_iter()
.flat_map(|clipped_primitive| {
let mut clip_rect_mesh = Mesh::default();
tessellator.tessellate_shape(
tex_size,
Shape::rect_stroke(clipped_primitive.clip_rect, 0.0, stroke),
&mut clip_rect_mesh,
);
[
clipped_primitive,
ClippedPrimitive {
clip_rect: Rect::EVERYTHING, // whatever
primitive: Primitive::Mesh(clip_rect_mesh),
},
]
})
.collect()
}