[user textures] Add custom texture/image support

* Each Traingles mesh comes with a TextureId
* ui.image(...) to show an image/texture
* Up to backend what to do with user textures
This commit is contained in:
Emil Ernerfeldt 2020-09-10 16:48:01 +02:00
parent 5ba420988f
commit 02ef0cd9d5
10 changed files with 201 additions and 72 deletions

View file

@ -53,9 +53,8 @@ TODO-list for the Egui project. If you looking for something to do, look here.
* [ ] Positioning preference: `window.preference(Top, Right)` * [ ] Positioning preference: `window.preference(Top, Right)`
* [ ] Keeping right/bottom on expand. Maybe cover jitteryness with quick animation? * [ ] Keeping right/bottom on expand. Maybe cover jitteryness with quick animation?
* [ ] Make auto-positioning of windows respect permanent side-bars. * [ ] Make auto-positioning of windows respect permanent side-bars.
* [ ] Image support * [/] Image support
* [ ] user-chosen texture ids (so people can show thing with mipmaps and whatnot) * [x] Show user textures
* [ ] `enum TextureId { Egui, User(u64) }` added to `Triangles`
* [ ] API for creating a texture managed by Egui * [ ] API for creating a texture managed by Egui
* Backend-agnostic. Good for people doing Egui-apps (games etc). * Backend-agnostic. Good for people doing Egui-apps (games etc).
* [ ] Convert font texture to RGBA, or communicate format in initialization? * [ ] Convert font texture to RGBA, or communicate format in initialization?

View file

@ -2,7 +2,7 @@
use crate::{ use crate::{
containers::show_tooltip, containers::show_tooltip,
math::*, math::*,
paint::{self, color::WHITE, PaintCmd, Texture, Triangles, Vertex}, paint::{self, color::WHITE, PaintCmd, Texture, Triangles},
*, *,
}; };
@ -21,18 +21,8 @@ impl Texture {
size *= ui.available().width() / size.x; size *= ui.available().width() / size.x;
} }
let rect = ui.allocate_space(size); let rect = ui.allocate_space(size);
let top_left = Vertex {
pos: rect.min,
uv: pos2(0.0, 0.0),
color: WHITE,
};
let bottom_right = Vertex {
pos: rect.max,
uv: pos2(1.0, 1.0),
color: WHITE,
};
let mut triangles = Triangles::default(); let mut triangles = Triangles::default();
triangles.add_rect(top_left, bottom_right); triangles.add_rect_with_uv(rect, [pos2(0.0, 0.0), pos2(1.0, 1.0)].into(), WHITE);
ui.painter().add(PaintCmd::Triangles(triangles)); ui.painter().add(PaintCmd::Triangles(triangles));
let tex_w = self.width as f32; let tex_w = self.width as f32;
@ -49,18 +39,12 @@ impl Texture {
let u = u.max(texel_radius).min(tex_w - texel_radius); let u = u.max(texel_radius).min(tex_w - texel_radius);
let v = v.max(texel_radius).min(tex_h - texel_radius); let v = v.max(texel_radius).min(tex_h - texel_radius);
let top_left = Vertex { let uv_rect = Rect::from_min_max(
pos: zoom_rect.min, pos2((u - texel_radius) / tex_w, (v - texel_radius) / tex_h),
uv: pos2((u - texel_radius) / tex_w, (v - texel_radius) / tex_h), pos2((u + texel_radius) / tex_w, (v + texel_radius) / tex_h),
color: WHITE, );
};
let bottom_right = Vertex {
pos: zoom_rect.max,
uv: pos2((u + texel_radius) / tex_w, (v + texel_radius) / tex_h),
color: WHITE,
};
let mut triangles = Triangles::default(); let mut triangles = Triangles::default();
triangles.add_rect(top_left, bottom_right); triangles.add_rect_with_uv(zoom_rect, uv_rect, WHITE);
ui.painter().add(PaintCmd::Triangles(triangles)); ui.painter().add(PaintCmd::Triangles(triangles));
}); });
} }
@ -70,7 +54,7 @@ impl Texture {
impl paint::FontDefinitions { impl paint::FontDefinitions {
pub fn ui(&mut self, ui: &mut Ui) { pub fn ui(&mut self, ui: &mut Ui) {
for (text_style, (_family, size)) in self.fonts.iter_mut() { for (text_style, (_family, size)) in self.fonts.iter_mut() {
// TODO: radiobutton for family // TODO: radio button for family
ui.add( ui.add(
Slider::f32(size, 4.0..=40.0) Slider::f32(size, 4.0..=40.0)
.precision(0) .precision(0)

View file

@ -75,7 +75,7 @@ pub use {
layout::*, layout::*,
math::*, math::*,
memory::Memory, memory::Memory,
paint::{color, PaintJobs, Rgba, Srgba, Stroke, TextStyle, Texture}, paint::{color, PaintJobs, Rgba, Srgba, Stroke, TextStyle, Texture, TextureId},
painter::Painter, painter::Painter,
style::Style, style::Style,
types::*, types::*,

View file

@ -199,3 +199,10 @@ impl std::fmt::Debug for Rect {
write!(f, "[{:?} - {:?}]", self.min, self.max) write!(f, "[{:?} - {:?}]", self.min, self.max)
} }
} }
/// from (min, max) or (left top, right bottom)
impl From<[Pos2; 2]> for Rect {
fn from([min, max]: [Pos2; 2]) -> Self {
Self { min, max }
}
}

View file

@ -13,6 +13,6 @@ pub use {
color::{Rgba, Srgba}, color::{Rgba, Srgba},
command::{PaintCmd, Stroke}, command::{PaintCmd, Stroke},
fonts::{FontDefinitions, Fonts, TextStyle}, fonts::{FontDefinitions, Fonts, TextStyle},
tessellator::{PaintJobs, PaintOptions, Triangles, Vertex, WHITE_UV}, tessellator::{PaintJobs, PaintOptions, TextureId, Triangles, Vertex, WHITE_UV},
texture_atlas::Texture, texture_atlas::Texture,
}; };

View file

@ -41,6 +41,10 @@ impl Srgba {
Self([0, 0, 0, a]) Self([0, 0, 0, a])
} }
pub fn white_alpha(a: u8) -> Self {
Rgba::white_alpha(linear_from_alpha_byte(a)).into()
}
pub const fn additive_luminance(l: u8) -> Self { pub const fn additive_luminance(l: u8) -> Self {
Self([l, l, l, 0]) Self([l, l, l, 0])
} }
@ -49,6 +53,27 @@ impl Srgba {
pub fn to_opaque(self) -> Self { pub fn to_opaque(self) -> Self {
Rgba::from(self).to_opaque().into() Rgba::from(self).to_opaque().into()
} }
pub fn r(&self) -> u8 {
self.0[0]
}
pub fn g(&self) -> u8 {
self.0[1]
}
pub fn b(&self) -> u8 {
self.0[2]
}
pub fn a(&self) -> u8 {
self.0[3]
}
pub fn to_array(&self) -> [u8; 4] {
[self.r(), self.g(), self.b(), self.a()]
}
pub fn to_tuple(&self) -> (u8, u8, u8, u8) {
(self.r(), self.g(), self.b(), self.a())
}
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------

View file

@ -14,6 +14,24 @@ use {
crate::math::*, crate::math::*,
}; };
/// What texture to use in a `Triangles` mesh.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum TextureId {
/// The Egui font texture.
/// If you don't want to use a texture, pick this and the `WHITE_UV` for uv-coord.
Egui,
/// Your own texture, defined in any which way you want.
/// Egui won't care. The backend renderer will presumably use this to look up what texture to use.
User(u64),
}
impl Default for TextureId {
fn default() -> Self {
Self::Egui
}
}
/// The UV coordinate of a white region of the texture mesh. /// The UV coordinate of a white region of the texture mesh.
/// The default Egui texture has the top-left corner pixel fully white. /// The default Egui texture has the top-left corner pixel fully white.
/// You need need use a clamping texture sampler for this to work /// You need need use a clamping texture sampler for this to work
@ -44,8 +62,12 @@ pub struct Vertex {
pub struct Triangles { pub struct Triangles {
/// Draw as triangles (i.e. the length is always multiple of three). /// Draw as triangles (i.e. the length is always multiple of three).
pub indices: Vec<u32>, pub indices: Vec<u32>,
/// The vertex data indexed by `indices`. /// The vertex data indexed by `indices`.
pub vertices: Vec<Vertex>, pub vertices: Vec<Vertex>,
/// The texture to use when drawing these triangles
pub texture_id: TextureId,
} }
/// A clip triangle and some textured triangles. /// A clip triangle and some textured triangles.
@ -58,14 +80,34 @@ pub type PaintJobs = Vec<PaintJob>;
/// ## Helpers for adding /// ## Helpers for adding
impl Triangles { impl Triangles {
pub fn with_texture(texture_id: TextureId) -> Self {
Self {
texture_id,
..Default::default()
}
}
/// Are all indices within the bounds of the contained vertices? /// Are all indices within the bounds of the contained vertices?
pub fn is_valid(&self) -> bool { pub fn is_valid(&self) -> bool {
let n = self.vertices.len() as u32; let n = self.vertices.len() as u32;
self.indices.iter().all(|&i| i < n) self.indices.iter().all(|&i| i < n)
} }
pub fn is_empty(&self) -> bool {
self.indices.is_empty() && self.vertices.is_empty()
}
/// Append all the indices and vertices of `other` to `self`. /// Append all the indices and vertices of `other` to `self`.
pub fn append(&mut self, other: &Triangles) { pub fn append(&mut self, other: &Triangles) {
if self.is_empty() {
self.texture_id = other.texture_id;
} else {
assert_eq!(
self.texture_id, other.texture_id,
"Can't merge Triangles using different textures"
);
}
let index_offset = self.vertices.len() as u32; let index_offset = self.vertices.len() as u32;
for index in &other.indices { for index in &other.indices {
self.indices.push(index_offset + index); self.indices.push(index_offset + index);
@ -74,6 +116,7 @@ impl Triangles {
} }
pub fn colored_vertex(&mut self, pos: Pos2, color: Srgba) { pub fn colored_vertex(&mut self, pos: Pos2, color: Srgba) {
debug_assert!(self.texture_id == TextureId::Egui);
self.vertices.push(Vertex { self.vertices.push(Vertex {
pos, pos,
uv: WHITE_UV, uv: WHITE_UV,
@ -100,44 +143,42 @@ impl Triangles {
self.vertices.reserve(additional); self.vertices.reserve(additional);
} }
/// Uniformly colored rectangle. /// Rectangle with a texture and color.
pub fn add_rect(&mut self, top_left: Vertex, bottom_right: Vertex) { pub fn add_rect_with_uv(&mut self, pos: Rect, uv: Rect, color: Srgba) {
debug_assert_eq!(top_left.color, bottom_right.color);
let idx = self.vertices.len() as u32; let idx = self.vertices.len() as u32;
self.add_triangle(idx + 0, idx + 1, idx + 2); self.add_triangle(idx + 0, idx + 1, idx + 2);
self.add_triangle(idx + 2, idx + 1, idx + 3); self.add_triangle(idx + 2, idx + 1, idx + 3);
let top_right = Vertex { let right_top = Vertex {
pos: pos2(bottom_right.pos.x, top_left.pos.y), pos: pos.right_top(),
uv: pos2(bottom_right.uv.x, top_left.uv.y), uv: uv.right_top(),
color: top_left.color, color,
}; };
let botom_left = Vertex { let left_top = Vertex {
pos: pos2(top_left.pos.x, bottom_right.pos.y), pos: pos.left_top(),
uv: pos2(top_left.uv.x, bottom_right.uv.y), uv: uv.left_top(),
color: top_left.color, color,
}; };
self.vertices.push(top_left); let left_bottom = Vertex {
self.vertices.push(top_right); pos: pos.left_bottom(),
self.vertices.push(botom_left); uv: uv.left_bottom(),
self.vertices.push(bottom_right); color,
};
let right_bottom = Vertex {
pos: pos.right_bottom(),
uv: uv.right_bottom(),
color,
};
self.vertices.push(left_top);
self.vertices.push(right_top);
self.vertices.push(left_bottom);
self.vertices.push(right_bottom);
} }
/// Uniformly colored rectangle. /// Uniformly colored rectangle.
pub fn add_colored_rect(&mut self, rect: Rect, color: Srgba) { pub fn add_colored_rect(&mut self, rect: Rect, color: Srgba) {
self.add_rect( debug_assert!(self.texture_id == TextureId::Egui);
Vertex { self.add_rect_with_uv(rect, [WHITE_UV, WHITE_UV].into(), color)
pos: rect.min,
uv: WHITE_UV,
color,
},
Vertex {
pos: rect.max,
uv: WHITE_UV,
color,
},
)
} }
/// This is for platforms that only support 16-bit index buffers. /// This is for platforms that only support 16-bit index buffers.
@ -189,6 +230,7 @@ impl Triangles {
.map(|vi| vi - min_vindex) .map(|vi| vi - min_vindex)
.collect(), .collect(),
vertices: self.vertices[(min_vindex as usize)..=(max_vindex as usize)].to_vec(), vertices: self.vertices[(min_vindex as usize)..=(max_vindex as usize)].to_vec(),
texture_id: self.texture_id,
}); });
} }
output output
@ -703,19 +745,17 @@ fn tessellate_paint_command(
for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) { for x_offset in line.x_offsets.iter().take(line.x_offsets.len() - 1) {
let c = chars.next().unwrap(); let c = chars.next().unwrap();
if let Some(glyph) = font.uv_rect(c) { if let Some(glyph) = font.uv_rect(c) {
let mut top_left = Vertex { let mut left_top =
pos: pos + glyph.offset + vec2(*x_offset, line.y_min) + text_offset, pos + glyph.offset + vec2(*x_offset, line.y_min) + text_offset;
uv: pos2(glyph.min.0 as f32 / tex_w, glyph.min.1 as f32 / tex_h), left_top.x = font.round_to_pixel(left_top.x); // Pixel-perfection.
color, left_top.y = font.round_to_pixel(left_top.y); // Pixel-perfection.
};
top_left.pos.x = font.round_to_pixel(top_left.pos.x); // Pixel-perfection. let pos = Rect::from_min_max(left_top, left_top + glyph.size);
top_left.pos.y = font.round_to_pixel(top_left.pos.y); // Pixel-perfection. let uv = Rect::from_min_max(
let bottom_right = Vertex { pos2(glyph.min.0 as f32 / tex_w, glyph.min.1 as f32 / tex_h),
pos: top_left.pos + glyph.size, pos2(glyph.max.0 as f32 / tex_w, glyph.max.1 as f32 / tex_h),
uv: pos2(glyph.max.0 as f32 / tex_w, glyph.max.1 as f32 / tex_h), );
color, out.add_rect_with_uv(pos, uv, color);
};
out.add_rect(top_left, bottom_right);
} }
} }
} }
@ -747,7 +787,16 @@ pub fn tessellate_paint_commands(
for (clip_rect, cmd) in commands { for (clip_rect, cmd) in commands {
// TODO: cull(clip_rect, cmd) // TODO: cull(clip_rect, cmd)
if jobs.is_empty() || jobs.last().unwrap().0 != clip_rect { if let PaintCmd::Triangles(triangles) = cmd {
// Assume non-Egui texture, which means own paint job:
jobs.push((clip_rect, triangles));
continue;
}
if jobs.is_empty()
|| jobs.last().unwrap().0 != clip_rect
|| jobs.last().unwrap().1.texture_id != TextureId::Egui
{
jobs.push((clip_rect, Triangles::default())); jobs.push((clip_rect, Triangles::default()));
} }

View file

@ -489,6 +489,11 @@ impl Ui {
let rect = self.allocate_space(desired_size); let rect = self.allocate_space(desired_size);
self.painter_at(rect) self.painter_at(rect)
} }
/// Show an image here with the given size
pub fn image(&mut self, texture_id: TextureId, desired_size: Vec2) -> Response {
self.add(Image::new(texture_id, desired_size))
}
} }
/// # Colors /// # Colors

View file

@ -9,10 +9,11 @@
use crate::{layout::Direction, *}; use crate::{layout::Direction, *};
pub mod color_picker; pub mod color_picker;
mod image;
mod slider; mod slider;
pub(crate) mod text_edit; pub(crate) mod text_edit;
pub use {slider::*, text_edit::*}; pub use {image::Image, slider::*, text_edit::*};
use paint::*; use paint::*;

59
egui/src/widgets/image.rs Normal file
View file

@ -0,0 +1,59 @@
use crate::*;
#[derive(Clone, Copy, Debug, Default)]
pub struct Image {
texture_id: TextureId,
desired_size: Vec2,
bg_fill: Srgba,
tint: Srgba,
}
impl Image {
pub fn new(texture_id: TextureId, desired_size: Vec2) -> Self {
Self {
texture_id,
desired_size,
tint: color::WHITE,
..Default::default()
}
}
/// A solid color to put behind the image. Useful for transparent images.
pub fn bg_fill(mut self, bg_fill: Srgba) -> Self {
self.bg_fill = bg_fill;
self
}
/// Multiply image color with this. Default is WHITE (no tint).
pub fn tint(mut self, tint: Srgba) -> Self {
self.tint = tint;
self
}
}
impl Widget for Image {
fn ui(self, ui: &mut Ui) -> Response {
use paint::*;
let Self {
texture_id,
desired_size,
bg_fill,
tint,
} = self;
let rect = ui.allocate_space(desired_size);
if bg_fill != Default::default() {
let mut triangles = Triangles::default();
triangles.add_colored_rect(rect, bg_fill);
ui.painter().add(PaintCmd::Triangles(triangles));
}
{
// TODO: builder pattern for Triangles
let uv = [pos2(0.0, 0.0), pos2(1.0, 1.0)];
let mut triangles = Triangles::with_texture(texture_id);
triangles.add_rect_with_uv(rect, uv.into(), tint);
ui.painter().add(PaintCmd::Triangles(triangles));
}
ui.interact_hover(rect)
}
}