Texture loading in egui (#1110)

* Move texture allocation into epaint/egui proper
* Add TextureHandle
* egui_glow: cast using bytemuck instead of unsafe code
* Optimize glium painter
* Optimize WebGL
* Add example of loading an image from file
This commit is contained in:
Emil Ernerfeldt 2022-01-15 13:59:52 +01:00 committed by GitHub
parent 6c616a1b69
commit 66d80e2519
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
59 changed files with 1297 additions and 860 deletions

View File

@ -8,6 +8,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
## Unreleased ## Unreleased
### Added ⭐ ### Added ⭐
* `Context::load_texture` to convert an image into a texture which can be displayed using e.g. `ui.image(texture, size)` ([#1110](https://github.com/emilk/egui/pull/1110)).
* Added `Ui::add_visible` and `Ui::add_visible_ui`. * Added `Ui::add_visible` and `Ui::add_visible_ui`.
### Changed 🔧 ### Changed 🔧
@ -18,6 +19,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
* `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)). * `Context` can now be cloned and stored between frames ([#1050](https://github.com/emilk/egui/pull/1050)).
* Renamed `Ui::visible` to `Ui::is_visible`. * Renamed `Ui::visible` to `Ui::is_visible`.
* Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)). * Split `Event::Text` into `Event::Text` and `Event::Paste` ([#1058](https://github.com/emilk/egui/pull/1058)).
* For integrations: `FontImage` has been replaced by `TexturesDelta` (found in `Output`), describing what textures were loaded and freed each frame ([#1110](https://github.com/emilk/egui/pull/1110)).
### Fixed 🐛 ### Fixed 🐛
* Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) * Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043))

4
Cargo.lock generated
View File

@ -841,6 +841,8 @@ dependencies = [
name = "egui_glium" name = "egui_glium"
version = "0.16.0" version = "0.16.0"
dependencies = [ dependencies = [
"ahash",
"bytemuck",
"egui", "egui",
"egui-winit", "egui-winit",
"epi", "epi",
@ -852,6 +854,7 @@ dependencies = [
name = "egui_glow" name = "egui_glow"
version = "0.16.0" version = "0.16.0"
dependencies = [ dependencies = [
"bytemuck",
"egui", "egui",
"egui-winit", "egui-winit",
"epi", "epi",
@ -867,6 +870,7 @@ dependencies = [
name = "egui_web" name = "egui_web"
version = "0.16.0" version = "0.16.0"
dependencies = [ dependencies = [
"bytemuck",
"egui", "egui",
"egui_glow", "egui_glow",
"epi", "epi",

View File

@ -5,6 +5,7 @@ NOTE: [`egui_web`](egui_web/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.m
## Unreleased ## Unreleased
* Removed `Frame::alloc_texture`. Use `egui::Context::load_texture` instead ([#1110](https://github.com/emilk/egui/pull/1110)).
* The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)). * The default native backend is now `egui_glow` (instead of `egui_glium`) ([#1020](https://github.com/emilk/egui/pull/1020)).
* The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)). * The default web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)).

View File

@ -4,7 +4,7 @@ use eframe::{egui, epi};
#[derive(Default)] #[derive(Default)]
struct MyApp { struct MyApp {
texture: Option<(egui::Vec2, egui::TextureId)>, texture: Option<egui::TextureHandle>,
} }
impl epi::App for MyApp { impl epi::App for MyApp {
@ -12,31 +12,18 @@ impl epi::App for MyApp {
"Show an image with eframe/egui" "Show an image with eframe/egui"
} }
fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) {
if self.texture.is_none() { let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
// Load the image: let image = load_image(include_bytes!("rust-logo-256x256.png")).unwrap();
let image_data = include_bytes!("rust-logo-256x256.png"); ctx.load_texture("rust-logo", image)
use image::GenericImageView; });
let image = image::load_from_memory(image_data).expect("Failed to load image");
let image_buffer = image.to_rgba8();
let size = [image.width() as usize, image.height() as usize];
let pixels = image_buffer.into_vec();
let image = epi::Image::from_rgba_unmultiplied(size, &pixels);
// Allocate a texture:
let texture = frame.alloc_texture(image);
let size = egui::Vec2::new(size[0] as f32, size[1] as f32);
self.texture = Some((size, texture));
}
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
if let Some((size, texture)) = self.texture {
ui.heading("This is an image:"); ui.heading("This is an image:");
ui.image(texture, size); ui.image(texture, texture.size_vec2());
ui.heading("This is an image you can click:"); ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(texture, size)); ui.add(egui::ImageButton::new(texture, texture.size_vec2()));
}
}); });
} }
} }
@ -45,3 +32,15 @@ fn main() {
let options = eframe::NativeOptions::default(); let options = eframe::NativeOptions::default();
eframe::run_native(Box::new(MyApp::default()), options); eframe::run_native(Box::new(MyApp::default()), options);
} }
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
use image::GenericImageView as _;
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}

View File

@ -55,10 +55,9 @@ pub fn handle_app_output(
window: &winit::window::Window, window: &winit::window::Window,
current_pixels_per_point: f32, current_pixels_per_point: f32,
app_output: epi::backend::AppOutput, app_output: epi::backend::AppOutput,
) -> epi::backend::TexAllocationData { ) {
let epi::backend::AppOutput { let epi::backend::AppOutput {
quit: _, quit: _,
tex_allocation_data,
window_size, window_size,
window_title, window_title,
decorated, decorated,
@ -86,8 +85,6 @@ pub fn handle_app_output(
if drag_window { if drag_window {
let _ = window.drag_window(); let _ = window.drag_window();
} }
tex_allocation_data
} }
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -244,16 +241,14 @@ impl EpiIntegration {
.setup(&self.egui_ctx, &self.frame, self.persistence.storage()); .setup(&self.egui_ctx, &self.frame, self.persistence.storage());
let app_output = self.frame.take_app_output(); let app_output = self.frame.take_app_output();
self.quit |= app_output.quit; self.quit |= app_output.quit;
let tex_alloc_data =
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output); crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
self.frame.lock().output.tex_allocation_data = tex_alloc_data; // Do it later
} }
fn warm_up(&mut self, window: &winit::window::Window) { fn warm_up(&mut self, window: &winit::window::Window) {
let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); let saved_memory: egui::Memory = self.egui_ctx.memory().clone();
self.egui_ctx.memory().set_everything_is_visible(true); self.egui_ctx.memory().set_everything_is_visible(true);
let (_, tex_alloc_data, _) = self.update(window); let (_, textures_delta, _) = self.update(window);
self.frame.lock().output.tex_allocation_data = tex_alloc_data; // handle it next frame self.egui_ctx.output().textures_delta = textures_delta; // Handle it next frame
*self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge. *self.egui_ctx.memory() = saved_memory; // We don't want to remember that windows were huge.
self.egui_ctx.clear_animations(); self.egui_ctx.clear_animations();
} }
@ -273,11 +268,7 @@ impl EpiIntegration {
pub fn update( pub fn update(
&mut self, &mut self,
window: &winit::window::Window, window: &winit::window::Window,
) -> ( ) -> (bool, egui::TexturesDelta, Vec<egui::epaint::ClippedShape>) {
bool,
epi::backend::TexAllocationData,
Vec<egui::epaint::ClippedShape>,
) {
let frame_start = instant::Instant::now(); let frame_start = instant::Instant::now();
let raw_input = self.egui_winit.take_egui_input(window); let raw_input = self.egui_winit.take_egui_input(window);
@ -286,18 +277,19 @@ impl EpiIntegration {
}); });
let needs_repaint = egui_output.needs_repaint; let needs_repaint = egui_output.needs_repaint;
self.egui_winit let textures_delta = self
.egui_winit
.handle_output(window, &self.egui_ctx, egui_output); .handle_output(window, &self.egui_ctx, egui_output);
let app_output = self.frame.take_app_output(); let app_output = self.frame.take_app_output();
self.quit |= app_output.quit; self.quit |= app_output.quit;
let tex_allocation_data =
crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output); crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output);
let frame_time = (instant::Instant::now() - frame_start).as_secs_f64() as f32; let frame_time = (instant::Instant::now() - frame_start).as_secs_f64() as f32;
self.frame.lock().info.cpu_usage = Some(frame_time); self.frame.lock().info.cpu_usage = Some(frame_time);
(needs_repaint, tex_allocation_data, shapes) (needs_repaint, textures_delta, shapes)
} }
pub fn maybe_autosave(&mut self, window: &winit::window::Window) { pub fn maybe_autosave(&mut self, window: &winit::window::Window) {

View File

@ -512,26 +512,39 @@ impl State {
window: &winit::window::Window, window: &winit::window::Window,
egui_ctx: &egui::Context, egui_ctx: &egui::Context,
output: egui::Output, output: egui::Output,
) { ) -> egui::TexturesDelta {
self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
if egui_ctx.memory().options.screen_reader { if egui_ctx.memory().options.screen_reader {
self.screen_reader.speak(&output.events_description()); self.screen_reader.speak(&output.events_description());
} }
self.set_cursor_icon(window, output.cursor_icon); let egui::Output {
cursor_icon,
open_url,
copied_text,
needs_repaint: _, // needs to be handled elsewhere
events: _, // handled above
mutable_text_under_cursor: _, // only used in egui_web
text_cursor_pos,
textures_delta,
} = output;
if let Some(open) = output.open_url { self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI
open_url(&open.url);
self.set_cursor_icon(window, cursor_icon);
if let Some(open_url) = open_url {
open_url_in_browser(&open_url.url);
} }
if !output.copied_text.is_empty() { if !copied_text.is_empty() {
self.clipboard.set(output.copied_text); self.clipboard.set(copied_text);
} }
if let Some(egui::Pos2 { x, y }) = output.text_cursor_pos { if let Some(egui::Pos2 { x, y }) = text_cursor_pos {
window.set_ime_position(winit::dpi::LogicalPosition { x, y }); window.set_ime_position(winit::dpi::LogicalPosition { x, y });
} }
textures_delta
} }
fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) { fn set_cursor_icon(&mut self, window: &winit::window::Window, cursor_icon: egui::CursorIcon) {
@ -554,7 +567,7 @@ impl State {
} }
} }
fn open_url(_url: &str) { fn open_url_in_browser(_url: &str) {
#[cfg(feature = "webbrowser")] #[cfg(feature = "webbrowser")]
if let Err(err) = webbrowser::open(_url) { if let Err(err) = webbrowser::open(_url) {
eprintln!("Failed to open url: {}", err); eprintln!("Failed to open url: {}", err);

View File

@ -2,18 +2,39 @@
use crate::{ use crate::{
animation_manager::AnimationManager, data::output::Output, frame_state::FrameState, animation_manager::AnimationManager, data::output::Output, frame_state::FrameState,
input_state::*, layers::GraphicLayers, *, input_state::*, layers::GraphicLayers, TextureHandle, *,
}; };
use epaint::{mutex::*, stats::*, text::Fonts, *}; use epaint::{mutex::*, stats::*, text::Fonts, *};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
struct WrappedTextureManager(Arc<RwLock<epaint::TextureManager>>);
impl Default for WrappedTextureManager {
fn default() -> Self {
let mut tex_mngr = epaint::textures::TextureManager::default();
// Will be filled in later
let font_id = tex_mngr.alloc(
"egui_font_texture".into(),
epaint::AlphaImage::new([0, 0]).into(),
);
assert_eq!(font_id, TextureId::default());
Self(Arc::new(RwLock::new(tex_mngr)))
}
}
// ----------------------------------------------------------------------------
#[derive(Default)] #[derive(Default)]
struct ContextImpl { struct ContextImpl {
/// `None` until the start of the first frame. /// `None` until the start of the first frame.
fonts: Option<Fonts>, fonts: Option<Fonts>,
memory: Memory, memory: Memory,
animation_manager: AnimationManager, animation_manager: AnimationManager,
latest_font_image_version: Option<u64>,
tex_manager: WrappedTextureManager,
input: InputState, input: InputState,
@ -157,7 +178,7 @@ impl Context {
/// ///
/// You can alternatively run [`Self::begin_frame`] and [`Context::end_frame`]. /// You can alternatively run [`Self::begin_frame`] and [`Context::end_frame`].
/// ///
/// ``` rust /// ```
/// // One egui context that you keep reusing: /// // One egui context that you keep reusing:
/// let mut ctx = egui::Context::default(); /// let mut ctx = egui::Context::default();
/// ///
@ -183,7 +204,7 @@ impl Context {
/// An alternative to calling [`Self::run`]. /// An alternative to calling [`Self::run`].
/// ///
/// ``` rust /// ```
/// // One egui context that you keep reusing: /// // One egui context that you keep reusing:
/// let mut ctx = egui::Context::default(); /// let mut ctx = egui::Context::default();
/// ///
@ -492,14 +513,6 @@ impl Context {
self.write().repaint_requests = 2; self.write().repaint_requests = 2;
} }
/// The egui font image, containing font characters etc.
///
/// Not valid until first call to [`Context::run()`].
/// That's because since we don't know the proper `pixels_per_point` until then.
pub fn font_image(&self) -> Arc<epaint::FontImage> {
self.fonts().font_image()
}
/// Tell `egui` which fonts to use. /// Tell `egui` which fonts to use.
/// ///
/// The default `egui` fonts only support latin and cyrillic alphabets, /// The default `egui` fonts only support latin and cyrillic alphabets,
@ -593,6 +606,54 @@ impl Context {
} }
} }
/// Allocate a texture.
///
/// In order to display an image you must convert it to a texture using this function.
///
/// Make sure to only call this once for each image, i.e. NOT in your main GUI code.
///
/// The given name can be useful for later debugging, and will be visible if you call [`Self::texture_ui`].
///
/// For how to load an image, see [`ImageData`] and [`ColorImage::from_rgba_unmultiplied`].
///
/// ```
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// Se also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::ImageButton`].
pub fn load_texture(
&self,
name: impl Into<String>,
image: impl Into<ImageData>,
) -> TextureHandle {
let tex_mngr = self.tex_manager();
let tex_id = tex_mngr.write().alloc(name.into(), image.into());
TextureHandle::new(tex_mngr, tex_id)
}
/// Low-level texture manager.
///
/// In general it is easier to use [`Self::load_texture`] and [`TextureHandle`].
///
/// You can show stats about the allocated textures using [`Self::texture_ui`].
pub fn tex_manager(&self) -> Arc<RwLock<epaint::textures::TextureManager>> {
self.read().tex_manager.0.clone()
}
// --------------------------------------------------------------------- // ---------------------------------------------------------------------
/// Constrain the position of a window/area so it fits within the provided boundary. /// Constrain the position of a window/area so it fits within the provided boundary.
@ -640,14 +701,30 @@ impl Context {
self.request_repaint(); self.request_repaint();
} }
self.fonts().end_frame();
{ {
let ctx_impl = &mut *self.write(); let ctx_impl = &mut *self.write();
ctx_impl ctx_impl
.memory .memory
.end_frame(&ctx_impl.input, &ctx_impl.frame_state.used_ids); .end_frame(&ctx_impl.input, &ctx_impl.frame_state.used_ids);
}
self.fonts().end_frame(); let font_image = ctx_impl.fonts.as_ref().unwrap().font_image();
let font_image_version = font_image.version;
if Some(font_image_version) != ctx_impl.latest_font_image_version {
ctx_impl
.tex_manager
.0
.write()
.set(TextureId::default(), font_image.image.clone().into());
ctx_impl.latest_font_image_version = Some(font_image_version);
}
ctx_impl
.output
.textures_delta
.append(ctx_impl.tex_manager.0.write().take_delta());
}
let mut output: Output = std::mem::take(&mut self.output()); let mut output: Output = std::mem::take(&mut self.output());
if self.read().repaint_requests > 0 { if self.read().repaint_requests > 0 {
@ -936,11 +1013,59 @@ impl Context {
}); });
CollapsingHeader::new("📊 Paint stats") CollapsingHeader::new("📊 Paint stats")
.default_open(true) .default_open(false)
.show(ui, |ui| { .show(ui, |ui| {
let paint_stats = self.write().paint_stats; let paint_stats = self.write().paint_stats;
paint_stats.ui(ui); paint_stats.ui(ui);
}); });
CollapsingHeader::new("🖼 Textures")
.default_open(false)
.show(ui, |ui| {
self.texture_ui(ui);
});
}
/// Show stats about the allocated textures.
pub fn texture_ui(&self, ui: &mut crate::Ui) {
let tex_mngr = self.tex_manager();
let tex_mngr = tex_mngr.read();
let mut textures: Vec<_> = tex_mngr.allocated().collect();
textures.sort_by_key(|(id, _)| *id);
let mut bytes = 0;
for (_, tex) in &textures {
bytes += tex.bytes_used();
}
ui.label(format!(
"{} allocated texture(s), using {:.1} MB",
textures.len(),
bytes as f64 * 1e-6
));
ui.group(|ui| {
ScrollArea::vertical()
.max_height(300.0)
.auto_shrink([false, true])
.show(ui, |ui| {
ui.style_mut().override_text_style = Some(TextStyle::Monospace);
Grid::new("textures")
.striped(true)
.num_columns(3)
.spacing(Vec2::new(16.0, 2.0))
.show(ui, |ui| {
for (_id, texture) in &textures {
let [w, h] = texture.size;
ui.label(format!("{} x {}", w, h));
ui.label(format!("{:.3} MB", texture.bytes_used() as f64 * 1e-6));
ui.label(format!("{:?}", texture.name));
ui.end_row();
}
});
});
});
} }
pub fn memory_ui(&self, ui: &mut crate::Ui) { pub fn memory_ui(&self, ui: &mut crate::Ui) {

View File

@ -375,10 +375,13 @@ impl RawInput {
} }
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("modifiers: {:#?}", modifiers)); ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("events: {:?}", events))
.on_hover_text("key presses etc");
ui.label(format!("hovered_files: {}", hovered_files.len())); ui.label(format!("hovered_files: {}", hovered_files.len()));
ui.label(format!("dropped_files: {}", dropped_files.len())); ui.label(format!("dropped_files: {}", dropped_files.len()));
ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events))
.on_hover_text("key presses etc");
});
} }
} }

View File

@ -35,6 +35,9 @@ pub struct Output {
/// Screen-space position of text edit cursor (used for IME). /// Screen-space position of text edit cursor (used for IME).
pub text_cursor_pos: Option<crate::Pos2>, pub text_cursor_pos: Option<crate::Pos2>,
/// Texture changes since last frame.
pub textures_delta: epaint::textures::TexturesDelta,
} }
impl Output { impl Output {
@ -71,6 +74,7 @@ impl Output {
mut events, mut events,
mutable_text_under_cursor, mutable_text_under_cursor,
text_cursor_pos, text_cursor_pos,
textures_delta,
} = newer; } = newer;
self.cursor_icon = cursor_icon; self.cursor_icon = cursor_icon;
@ -84,6 +88,7 @@ impl Output {
self.events.append(&mut events); self.events.append(&mut events);
self.mutable_text_under_cursor = mutable_text_under_cursor; self.mutable_text_under_cursor = mutable_text_under_cursor;
self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos); self.text_cursor_pos = text_cursor_pos.or(self.text_cursor_pos);
self.textures_delta.append(textures_delta);
} }
/// Take everything ephemeral (everything except `cursor_icon` currently) /// Take everything ephemeral (everything except `cursor_icon` currently)

View File

@ -730,8 +730,11 @@ impl InputState {
ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt));
ui.label(format!("modifiers: {:#?}", modifiers)); ui.label(format!("modifiers: {:#?}", modifiers));
ui.label(format!("keys_down: {:?}", keys_down)); ui.label(format!("keys_down: {:?}", keys_down));
ui.label(format!("events: {:?}", events)) ui.scope(|ui| {
ui.set_min_height(150.0);
ui.label(format!("events: {:#?}", events))
.on_hover_text("key presses etc"); .on_hover_text("key presses etc");
});
} }
} }

View File

@ -7,14 +7,16 @@ impl Widget for &epaint::FontImage {
ui.vertical(|ui| { ui.vertical(|ui| {
// Show font texture in demo Ui // Show font texture in demo Ui
let [width, height] = self.size();
ui.label(format!( ui.label(format!(
"Texture size: {} x {} (hover to zoom)", "Texture size: {} x {} (hover to zoom)",
self.width, self.height width, height
)); ));
if self.width <= 1 || self.height <= 1 { if width <= 1 || height <= 1 {
return; return;
} }
let mut size = vec2(self.width as f32, self.height as f32); let mut size = vec2(width as f32, height as f32);
if size.x > ui.available_width() { if size.x > ui.available_width() {
size *= ui.available_width() / size.x; size *= ui.available_width() / size.x;
} }
@ -27,7 +29,7 @@ impl Widget for &epaint::FontImage {
); );
ui.painter().add(Shape::mesh(mesh)); ui.painter().add(Shape::mesh(mesh));
let (tex_w, tex_h) = (self.width as f32, self.height as f32); let (tex_w, tex_h) = (width as f32, height as f32);
response response
.on_hover_cursor(CursorIcon::ZoomIn) .on_hover_cursor(CursorIcon::ZoomIn)

View File

@ -58,7 +58,7 @@
//! //!
//! ### Quick start //! ### Quick start
//! //!
//! ``` rust //! ```
//! # egui::__run_test_ui(|ui| { //! # egui::__run_test_ui(|ui| {
//! # let mut my_string = String::new(); //! # let mut my_string = String::new();
//! # let mut my_boolean = true; //! # let mut my_boolean = true;
@ -218,7 +218,7 @@
//! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`]. //! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`].
//! 3. Use a justified layout: //! 3. Use a justified layout:
//! //!
//! ``` rust //! ```
//! # egui::__run_test_ui(|ui| { //! # egui::__run_test_ui(|ui| {
//! ui.with_layout(egui::Layout::top_down_justified(egui::Align::Center), |ui| { //! ui.with_layout(egui::Layout::top_down_justified(egui::Align::Center), |ui| {
//! ui.button("I am becoming wider as needed"); //! ui.button("I am becoming wider as needed");
@ -228,7 +228,7 @@
//! //!
//! 4. Fill in extra space with emptiness: //! 4. Fill in extra space with emptiness:
//! //!
//! ``` rust //! ```
//! # egui::__run_test_ui(|ui| { //! # egui::__run_test_ui(|ui| {
//! ui.allocate_space(ui.available_size()); // put this LAST in your panel/window code //! ui.allocate_space(ui.available_size()); // put this LAST in your panel/window code
//! # }); //! # });
@ -386,7 +386,9 @@ pub use emath::{lerp, pos2, remap, remap_clamp, vec2, Align, Align2, NumExt, Pos
pub use epaint::{ pub use epaint::{
color, mutex, color, mutex,
text::{FontData, FontDefinitions, FontFamily, TextStyle}, text::{FontData, FontDefinitions, FontFamily, TextStyle},
ClippedMesh, Color32, FontImage, Rgba, Shape, Stroke, TextureId, textures::TexturesDelta,
AlphaImage, ClippedMesh, Color32, ColorImage, ImageData, Rgba, Shape, Stroke, TextureHandle,
TextureId,
}; };
pub mod text { pub mod text {

View File

@ -480,7 +480,7 @@ impl Response {
/// Response to secondary clicks (right-clicks) by showing the given menu. /// Response to secondary clicks (right-clicks) by showing the given menu.
/// ///
/// ``` rust /// ```
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// let response = ui.label("Right-click me!"); /// let response = ui.label("Right-click me!");
/// response.context_menu(|ui| { /// response.context_menu(|ui| {

View File

@ -1341,9 +1341,30 @@ impl Ui {
/// Show an image here with the given size. /// Show an image here with the given size.
/// ///
/// See also [`Image`]. /// In order to display an image you must first acquire a [`TextureHandle`]
/// using [`Context::load_texture`].
///
/// ```
/// struct MyImage {
/// texture: Option<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// Se also [`crate::Image`] and [`crate::ImageButton`].
#[inline] #[inline]
pub fn image(&mut self, texture_id: TextureId, size: impl Into<Vec2>) -> Response { pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
Image::new(texture_id, size).ui(self) Image::new(texture_id, size).ui(self)
} }
} }

View File

@ -109,11 +109,11 @@ where
/// `(time, value)` pairs /// `(time, value)` pairs
/// Time difference between values can be zero, but never negative. /// Time difference between values can be zero, but never negative.
// TODO: impl IntoIter // TODO: impl IntoIter
pub fn iter(&'_ self) -> impl Iterator<Item = (f64, T)> + '_ { pub fn iter(&'_ self) -> impl ExactSizeIterator<Item = (f64, T)> + '_ {
self.values.iter().map(|(time, value)| (*time, *value)) self.values.iter().map(|(time, value)| (*time, *value))
} }
pub fn values(&'_ self) -> impl Iterator<Item = T> + '_ { pub fn values(&'_ self) -> impl ExactSizeIterator<Item = T> + '_ {
self.values.iter().map(|(_time, value)| *value) self.values.iter().map(|(_time, value)| *value)
} }

View File

@ -394,7 +394,7 @@ pub struct ImageButton {
} }
impl ImageButton { impl ImageButton {
pub fn new(texture_id: TextureId, size: impl Into<Vec2>) -> Self { pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
Self { Self {
image: widgets::Image::new(texture_id, size), image: widgets::Image::new(texture_id, size),
sense: Sense::click(), sense: Sense::click(),

View File

@ -2,17 +2,31 @@ use crate::*;
/// An widget to show an image of a given size. /// An widget to show an image of a given size.
/// ///
/// In order to display an image you must first acquire a [`TextureHandle`]
/// using [`Context::load_texture`].
///
/// ``` /// ```
/// # egui::__run_test_ui(|ui| { /// struct MyImage {
/// # let my_texture_id = egui::TextureId::User(0); /// texture: Option<egui::TextureHandle>,
/// ui.add(egui::Image::new(my_texture_id, [640.0, 480.0])); /// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.add(egui::Image::new(texture, texture.size_vec2()));
/// ///
/// // Shorter version: /// // Shorter version:
/// ui.image(my_texture_id, [640.0, 480.0]); /// ui.image(texture, texture.size_vec2());
/// # }); /// }
/// }
/// ``` /// ```
/// ///
/// Se also [`crate::ImageButton`]. /// Se also [`crate::Ui::image`] and [`crate::ImageButton`].
#[must_use = "You should put this widget in an ui with `ui.add(widget);`"] #[must_use = "You should put this widget in an ui with `ui.add(widget);`"]
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct Image { pub struct Image {
@ -25,9 +39,9 @@ pub struct Image {
} }
impl Image { impl Image {
pub fn new(texture_id: TextureId, size: impl Into<Vec2>) -> Self { pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Self {
Self { Self {
texture_id, texture_id: texture_id.into(),
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
size: size.into(), size: size.into(),
bg_fill: Default::default(), bg_fill: Default::default(),

View File

@ -49,7 +49,7 @@ impl Label {
/// By calling this you can turn the label into a button of sorts. /// By calling this you can turn the label into a button of sorts.
/// This will also give the label the hover-effect of a button, but without the frame. /// This will also give the label the hover-effect of a button, but without the frame.
/// ///
/// ``` rust /// ```
/// # use egui::{Label, Sense}; /// # use egui::{Label, Sense};
/// # egui::__run_test_ui(|ui| { /// # egui::__run_test_ui(|ui| {
/// if ui.add(Label::new("click me").sense(Sense::click())).clicked() { /// if ui.add(Label::new("click me").sense(Sense::click())).clicked() {
@ -82,7 +82,9 @@ impl Label {
} }
let valign = ui.layout().vertical_align(); let valign = ui.layout().vertical_align();
let mut text_job = self.text.into_text_job(ui.style(), TextStyle::Body, valign); let mut text_job = self
.text
.into_text_job(ui.style(), ui.style().body_text_style, valign);
let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text()); let should_wrap = self.wrap.unwrap_or_else(|| ui.wrap_text());
let available_width = ui.available_width(); let available_width = ui.available_width();

View File

@ -1087,12 +1087,12 @@ pub struct PlotImage {
impl PlotImage { impl PlotImage {
/// Create a new image with position and size in plot coordinates. /// Create a new image with position and size in plot coordinates.
pub fn new(texture_id: TextureId, position: Value, size: impl Into<Vec2>) -> Self { pub fn new(texture_id: impl Into<TextureId>, position: Value, size: impl Into<Vec2>) -> Self {
Self { Self {
position, position,
name: Default::default(), name: Default::default(),
highlight: false, highlight: false,
texture_id, texture_id: texture_id.into(),
uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)),
size: size.into(), size: size.into(),
bg_fill: Default::default(), bg_fill: Default::default(),

View File

@ -297,7 +297,7 @@ pub enum MarkerShape {
impl MarkerShape { impl MarkerShape {
/// Get a vector containing all marker shapes. /// Get a vector containing all marker shapes.
pub fn all() -> impl Iterator<Item = MarkerShape> { pub fn all() -> impl ExactSizeIterator<Item = MarkerShape> {
[ [
Self::Circle, Self::Circle,
Self::Diamond, Self::Diamond,

View File

@ -43,14 +43,14 @@ impl epi::App for ColorTest {
ui.separator(); ui.separator();
} }
ScrollArea::both().auto_shrink([false; 2]).show(ui, |ui| { ScrollArea::both().auto_shrink([false; 2]).show(ui, |ui| {
self.ui(ui, Some(frame)); self.ui(ui);
}); });
}); });
} }
} }
impl ColorTest { impl ColorTest {
pub fn ui(&mut self, ui: &mut Ui, tex_allocator: Option<&dyn epi::TextureAllocator>) { pub fn ui(&mut self, ui: &mut Ui) {
ui.set_max_width(680.0); ui.set_max_width(680.0);
ui.vertical_centered(|ui| { ui.vertical_centered(|ui| {
@ -70,13 +70,7 @@ impl ColorTest {
ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients
let g = Gradient::one_color(Color32::from_rgb(255, 165, 0)); let g = Gradient::one_color(Color32::from_rgb(255, 165, 0));
self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g); self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g);
self.tex_gradient( self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g);
ui,
tex_allocator,
"orange rgb(255, 165, 0) - texture",
WHITE,
&g,
);
}); });
ui.separator(); ui.separator();
@ -99,20 +93,18 @@ impl ColorTest {
{ {
let g = Gradient::one_color(Color32::from(tex_color * vertex_color)); let g = Gradient::one_color(Color32::from(tex_color * vertex_color));
self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g);
self.tex_gradient(ui, tex_allocator, "Ground truth (texture)", WHITE, &g); self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g);
} }
if let Some(tex_allocator) = tex_allocator {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let g = Gradient::one_color(Color32::from(tex_color)); let g = Gradient::one_color(Color32::from(tex_color));
let tex = self.tex_mngr.get(tex_allocator, &g); let tex = self.tex_mngr.get(ui.ctx(), &g);
let texel_offset = 0.5 / (g.0.len() as f32); let texel_offset = 0.5 / (g.0.len() as f32);
let uv = let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv)) ui.add(Image::new(tex, GRADIENT_SIZE).tint(vertex_color).uv(uv))
.on_hover_text(format!("A texture that is {} texels wide", g.0.len())); .on_hover_text(format!("A texture that is {} texels wide", g.0.len()));
ui.label("GPU result"); ui.label("GPU result");
}); });
}
}); });
ui.separator(); ui.separator();
@ -120,18 +112,18 @@ impl ColorTest {
// TODO: test color multiplication (image tint), // TODO: test color multiplication (image tint),
// to make sure vertex and texture color multiplication is done in linear space. // to make sure vertex and texture color multiplication is done in linear space.
self.show_gradients(ui, tex_allocator, WHITE, (RED, GREEN)); self.show_gradients(ui, WHITE, (RED, GREEN));
if self.srgb { if self.srgb {
ui.label("Notice the darkening in the center of the naive sRGB interpolation."); ui.label("Notice the darkening in the center of the naive sRGB interpolation.");
} }
ui.separator(); ui.separator();
self.show_gradients(ui, tex_allocator, RED, (TRANSPARENT, GREEN)); self.show_gradients(ui, RED, (TRANSPARENT, GREEN));
ui.separator(); ui.separator();
self.show_gradients(ui, tex_allocator, WHITE, (TRANSPARENT, GREEN)); self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN));
if self.srgb { if self.srgb {
ui.label( ui.label(
"Notice how the linear blend stays green while the naive sRGBA interpolation looks gray in the middle.", "Notice how the linear blend stays green while the naive sRGBA interpolation looks gray in the middle.",
@ -142,15 +134,14 @@ impl ColorTest {
// TODO: another ground truth where we do the alpha-blending against the background also. // TODO: another ground truth where we do the alpha-blending against the background also.
// TODO: exactly the same thing, but with vertex colors (no textures) // TODO: exactly the same thing, but with vertex colors (no textures)
self.show_gradients(ui, tex_allocator, WHITE, (TRANSPARENT, BLACK)); self.show_gradients(ui, WHITE, (TRANSPARENT, BLACK));
ui.separator(); ui.separator();
self.show_gradients(ui, tex_allocator, BLACK, (TRANSPARENT, WHITE)); self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE));
ui.separator(); ui.separator();
ui.label("Additive blending: add more and more blue to the red background:"); ui.label("Additive blending: add more and more blue to the red background:");
self.show_gradients( self.show_gradients(
ui, ui,
tex_allocator,
RED, RED,
(TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)), (TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)),
); );
@ -160,13 +151,7 @@ impl ColorTest {
pixel_test(ui); pixel_test(ui);
} }
fn show_gradients( fn show_gradients(&mut self, ui: &mut Ui, bg_fill: Color32, (left, right): (Color32, Color32)) {
&mut self,
ui: &mut Ui,
tex_allocator: Option<&dyn epi::TextureAllocator>,
bg_fill: Color32,
(left, right): (Color32, Color32),
) {
let is_opaque = left.is_opaque() && right.is_opaque(); let is_opaque = left.is_opaque() && right.is_opaque();
ui.horizontal(|ui| { ui.horizontal(|ui| {
@ -186,13 +171,7 @@ impl ColorTest {
if is_opaque { if is_opaque {
let g = Gradient::ground_truth_linear_gradient(left, right); let g = Gradient::ground_truth_linear_gradient(left, right);
self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g); self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g);
self.tex_gradient( self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g);
ui,
tex_allocator,
"Ground Truth (CPU gradient) - texture",
bg_fill,
&g,
);
} else { } else {
let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill); let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill);
self.vertex_gradient( self.vertex_gradient(
@ -203,20 +182,13 @@ impl ColorTest {
); );
self.tex_gradient( self.tex_gradient(
ui, ui,
tex_allocator,
"Ground Truth (CPU gradient, CPU blending) - texture", "Ground Truth (CPU gradient, CPU blending) - texture",
bg_fill, bg_fill,
&g, &g,
); );
let g = Gradient::ground_truth_linear_gradient(left, right); let g = Gradient::ground_truth_linear_gradient(left, right);
self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g); self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g);
self.tex_gradient( self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g);
ui,
tex_allocator,
"CPU gradient, GPU blending - texture",
bg_fill,
&g,
);
} }
let g = Gradient::texture_gradient(left, right); let g = Gradient::texture_gradient(left, right);
@ -226,13 +198,7 @@ impl ColorTest {
bg_fill, bg_fill,
&g, &g,
); );
self.tex_gradient( self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g);
ui,
tex_allocator,
"Texture of width 2 (test texture sampler)",
bg_fill,
&g,
);
if self.srgb { if self.srgb {
let g = let g =
@ -243,31 +209,17 @@ impl ColorTest {
bg_fill, bg_fill,
&g, &g,
); );
self.tex_gradient( self.tex_gradient(ui, "Naive sRGBA interpolation (WRONG)", bg_fill, &g);
ui,
tex_allocator,
"Naive sRGBA interpolation (WRONG)",
bg_fill,
&g,
);
} }
}); });
} }
fn tex_gradient( fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
&mut self,
ui: &mut Ui,
tex_allocator: Option<&dyn epi::TextureAllocator>,
label: &str,
bg_fill: Color32,
gradient: &Gradient,
) {
if !self.texture_gradients { if !self.texture_gradients {
return; return;
} }
if let Some(tex_allocator) = tex_allocator {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let tex = self.tex_mngr.get(tex_allocator, gradient); let tex = self.tex_mngr.get(ui.ctx(), gradient);
let texel_offset = 0.5 / (gradient.0.len() as f32); let texel_offset = 0.5 / (gradient.0.len() as f32);
let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0)); let uv = Rect::from_min_max(pos2(texel_offset, 0.0), pos2(1.0 - texel_offset, 1.0));
ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv)) ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv))
@ -278,7 +230,6 @@ impl ColorTest {
ui.label(label); ui.label(label);
}); });
} }
}
fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) { fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) {
if !self.vertex_gradients { if !self.vertex_gradients {
@ -384,18 +335,21 @@ impl Gradient {
} }
#[derive(Default)] #[derive(Default)]
struct TextureManager(HashMap<Gradient, TextureId>); struct TextureManager(HashMap<Gradient, TextureHandle>);
impl TextureManager { impl TextureManager {
fn get(&mut self, tex_allocator: &dyn epi::TextureAllocator, gradient: &Gradient) -> TextureId { fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle {
*self.0.entry(gradient.clone()).or_insert_with(|| { self.0.entry(gradient.clone()).or_insert_with(|| {
let pixels = gradient.to_pixel_row(); let pixels = gradient.to_pixel_row();
let width = pixels.len(); let width = pixels.len();
let height = 1; let height = 1;
tex_allocator.alloc(epi::Image { ctx.load_texture(
"color_test_gradient",
epaint::ColorImage {
size: [width, height], size: [width, height],
pixels, pixels,
}) },
)
}) })
} }
} }

View File

@ -306,9 +306,9 @@ impl Widget for &mut LegendDemo {
} }
#[derive(PartialEq, Default)] #[derive(PartialEq, Default)]
struct ItemsDemo {} struct ItemsDemo {
texture: Option<egui::TextureHandle>,
impl ItemsDemo {} }
impl Widget for &mut ItemsDemo { impl Widget for &mut ItemsDemo {
fn ui(self, ui: &mut Ui) -> Response { fn ui(self, ui: &mut Ui) -> Response {
@ -343,12 +343,17 @@ impl Widget for &mut ItemsDemo {
); );
Arrows::new(arrow_origins, arrow_tips) Arrows::new(arrow_origins, arrow_tips)
}; };
let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
ui.ctx()
.load_texture("plot_demo", egui::ColorImage::example())
});
let image = PlotImage::new( let image = PlotImage::new(
TextureId::Egui, texture,
Value::new(0.0, 10.0), Value::new(0.0, 10.0),
[ [
ui.fonts().font_image().width as f32 / 100.0, ui.fonts().font_image().width() as f32 / 100.0,
ui.fonts().font_image().height as f32 / 100.0, ui.fonts().font_image().height() as f32 / 100.0,
], ],
); );

View File

@ -17,6 +17,8 @@ pub struct WidgetGallery {
string: String, string: String,
color: egui::Color32, color: egui::Color32,
animate_progress_bar: bool, animate_progress_bar: bool,
#[cfg_attr(feature = "serde", serde(skip))]
texture: Option<egui::TextureHandle>,
} }
impl Default for WidgetGallery { impl Default for WidgetGallery {
@ -30,6 +32,7 @@ impl Default for WidgetGallery {
string: Default::default(), string: Default::default(),
color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5), color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5),
animate_progress_bar: false, animate_progress_bar: false,
texture: None,
} }
} }
} }
@ -99,8 +102,14 @@ impl WidgetGallery {
string, string,
color, color,
animate_progress_bar, animate_progress_bar,
texture,
} = self; } = self;
let texture: &egui::TextureHandle = texture.get_or_insert_with(|| {
ui.ctx()
.load_texture("example", egui::ColorImage::example())
});
ui.add(doc_link_label("Label", "label,heading")); ui.add(doc_link_label("Label", "label,heading"));
ui.label("Welcome to the widget gallery!"); ui.label("Welcome to the widget gallery!");
ui.end_row(); ui.end_row();
@ -180,17 +189,14 @@ impl WidgetGallery {
ui.color_edit_button_srgba(color); ui.color_edit_button_srgba(color);
ui.end_row(); ui.end_row();
let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y;
ui.add(doc_link_label("Image", "Image")); ui.add(doc_link_label("Image", "Image"));
ui.image(egui::TextureId::Egui, [24.0, 16.0]) ui.image(texture, img_size);
.on_hover_text("The egui font texture was the convenient choice to show here.");
ui.end_row(); ui.end_row();
ui.add(doc_link_label("ImageButton", "ImageButton")); ui.add(doc_link_label("ImageButton", "ImageButton"));
if ui if ui.add(egui::ImageButton::new(texture, img_size)).clicked() {
.add(egui::ImageButton::new(egui::TextureId::Egui, [24.0, 16.0]))
.on_hover_text("The egui font texture was the convenient choice to show here.")
.clicked()
{
*boolean = !*boolean; *boolean = !*boolean;
} }
ui.end_row(); ui.end_row();

View File

@ -7,7 +7,7 @@ struct Resource {
text: Option<String>, text: Option<String>,
/// If set, the response was an image. /// If set, the response was an image.
image: Option<epi::Image>, image: Option<egui::ImageData>,
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
colored_text: Option<ColoredText>, colored_text: Option<ColoredText>,
@ -17,7 +17,7 @@ impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self { fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default(); let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") { let image = if content_type.starts_with("image/") {
decode_image(&response.bytes) load_image(&response.bytes).ok().map(|img| img.into())
} else { } else {
None None
}; };
@ -112,7 +112,7 @@ impl epi::App for HttpApp {
} else if let Some(result) = &self.result { } else if let Some(result) = &self.result {
match result { match result {
Ok(resource) => { Ok(resource) => {
ui_resource(ui, frame, &mut self.tex_mngr, resource); ui_resource(ui, &mut self.tex_mngr, resource);
} }
Err(error) => { Err(error) => {
// This should only happen if the fetch API isn't available or something similar. // This should only happen if the fetch API isn't available or something similar.
@ -160,7 +160,7 @@ fn ui_url(ui: &mut egui::Ui, frame: &epi::Frame, url: &mut String) -> bool {
trigger_fetch trigger_fetch
} }
fn ui_resource(ui: &mut egui::Ui, frame: &epi::Frame, tex_mngr: &mut TexMngr, resource: &Resource) { fn ui_resource(ui: &mut egui::Ui, tex_mngr: &mut TexMngr, resource: &Resource) {
let Resource { let Resource {
response, response,
text, text,
@ -212,11 +212,10 @@ fn ui_resource(ui: &mut egui::Ui, frame: &epi::Frame, tex_mngr: &mut TexMngr, re
} }
if let Some(image) = image { if let Some(image) = image {
if let Some(texture_id) = tex_mngr.texture(frame, &response.url, image) { let texture = tex_mngr.texture(ui.ctx(), &response.url, image);
let mut size = egui::Vec2::new(image.size[0] as f32, image.size[1] as f32); let mut size = texture.size_vec2();
size *= (ui.available_width() / size.x).min(1.0); size *= (ui.available_width() / size.x).min(1.0);
ui.image(texture_id, size); ui.image(texture, size);
}
} else if let Some(colored_text) = colored_text { } else if let Some(colored_text) = colored_text {
colored_text.ui(ui); colored_text.ui(ui);
} else if let Some(text) = &text { } else if let Some(text) = &text {
@ -293,33 +292,32 @@ impl ColoredText {
#[derive(Default)] #[derive(Default)]
struct TexMngr { struct TexMngr {
loaded_url: String, loaded_url: String,
texture_id: Option<egui::TextureId>, texture: Option<egui::TextureHandle>,
} }
impl TexMngr { impl TexMngr {
fn texture( fn texture(
&mut self, &mut self,
frame: &epi::Frame, ctx: &egui::Context,
url: &str, url: &str,
image: &epi::Image, image: &egui::ImageData,
) -> Option<egui::TextureId> { ) -> &egui::TextureHandle {
if self.loaded_url != url { if self.loaded_url != url || self.texture.is_none() {
if let Some(texture_id) = self.texture_id.take() { self.texture = Some(ctx.load_texture(url, image.clone()));
frame.free_texture(texture_id);
}
self.texture_id = Some(frame.alloc_texture(image.clone()));
self.loaded_url = url.to_owned(); self.loaded_url = url.to_owned();
} }
self.texture_id self.texture.as_ref().unwrap()
} }
} }
fn decode_image(bytes: &[u8]) -> Option<epi::Image> { fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
use image::GenericImageView; use image::GenericImageView as _;
let image = image::load_from_memory(bytes).ok()?; let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8(); let image_buffer = image.to_rgba8();
let size = [image.width() as usize, image.height() as usize]; let pixels = image_buffer.as_flat_samples();
let pixels = image_buffer.into_vec(); Ok(egui::ColorImage::from_rgba_unmultiplied(
Some(epi::Image::from_rgba_unmultiplied(size, &pixels)) size,
pixels.as_slice(),
))
} }

View File

@ -66,7 +66,7 @@ enum SyntectTheme {
#[cfg(feature = "syntect")] #[cfg(feature = "syntect")]
impl SyntectTheme { impl SyntectTheme {
fn all() -> impl Iterator<Item = Self> { fn all() -> impl ExactSizeIterator<Item = Self> {
[ [
Self::Base16EightiesDark, Self::Base16EightiesDark,
Self::Base16MochaDark, Self::Base16MochaDark,

View File

@ -3,6 +3,8 @@ All notable changes to the `egui_glium` integration will be noted in this file.
## Unreleased ## Unreleased
* `EguiGlium::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlium::paint` ([#1110](https://github.com/emilk/egui/pull/1110)).
* Optimize the painter and texture uploading ([#1110](https://github.com/emilk/egui/pull/1110)).
## 0.16.0 - 2021-12-29 ## 0.16.0 - 2021-12-29

View File

@ -23,10 +23,15 @@ include = [
all-features = true all-features = true
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = ["single_threaded"] } egui = { version = "0.16.0", path = "../egui", default-features = false, features = [
"convert_bytemuck",
"single_threaded",
] }
egui-winit = { version = "0.16.0", path = "../egui-winit", default-features = false, features = ["epi"] } egui-winit = { version = "0.16.0", path = "../egui-winit", default-features = false, features = ["epi"] }
epi = { version = "0.16.0", path = "../epi", optional = true } epi = { version = "0.16.0", path = "../epi", optional = true }
ahash = "0.7"
bytemuck = "1.7"
glium = "0.31" glium = "0.31"
[dev-dependencies] [dev-dependencies]

View File

@ -62,7 +62,7 @@ fn main() {
let mut redraw = || { let mut redraw = || {
let mut quit = false; let mut quit = false;
let (needs_repaint, shapes) = egui_glium.run(&display, |egui_ctx| { let needs_repaint = egui_glium.run(&display, |egui_ctx| {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
if ui if ui
.add(egui::Button::image_and_text( .add(egui::Button::image_and_text(
@ -98,7 +98,7 @@ fn main() {
// draw things behind egui here // draw things behind egui here
egui_glium.paint(&display, &mut target, shapes); egui_glium.paint(&display, &mut target);
// draw things on top of egui here // draw things on top of egui here

View File

@ -32,7 +32,7 @@ fn main() {
let mut redraw = || { let mut redraw = || {
let mut quit = false; let mut quit = false;
let (needs_repaint, shapes) = egui_glium.run(&display, |egui_ctx| { let needs_repaint = egui_glium.run(&display, |egui_ctx| {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
ui.heading("Hello World!"); ui.heading("Hello World!");
if ui.button("Quit").clicked() { if ui.button("Quit").clicked() {
@ -59,7 +59,7 @@ fn main() {
// draw things behind egui here // draw things behind egui here
egui_glium.paint(&display, &mut target, shapes); egui_glium.paint(&display, &mut target);
// draw things on top of egui here // draw things on top of egui here

View File

@ -66,11 +66,11 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
let (needs_repaint, mut tex_allocation_data, shapes) = let (needs_repaint, mut textures_delta, shapes) =
integration.update(display.gl_window().window()); integration.update(display.gl_window().window());
let clipped_meshes = integration.egui_ctx.tessellate(shapes); let clipped_meshes = integration.egui_ctx.tessellate(shapes);
for (id, image) in tex_allocation_data.creations { for (id, image) in textures_delta.set {
painter.set_texture(&display, id, &image); painter.set_texture(&display, id, &image);
} }
@ -86,13 +86,12 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
&mut target, &mut target,
integration.egui_ctx.pixels_per_point(), integration.egui_ctx.pixels_per_point(),
clipped_meshes, clipped_meshes,
&integration.egui_ctx.font_image(),
); );
target.finish().unwrap(); target.finish().unwrap();
} }
for id in tex_allocation_data.destructions.drain(..) { for id in textures_delta.free.drain(..) {
painter.free_texture(id); painter.free_texture(id);
} }

View File

@ -104,6 +104,9 @@ pub struct EguiGlium {
pub egui_ctx: egui::Context, pub egui_ctx: egui::Context,
pub egui_winit: egui_winit::State, pub egui_winit: egui_winit::State,
pub painter: crate::Painter, pub painter: crate::Painter,
shapes: Vec<egui::epaint::ClippedShape>,
textures_delta: egui::TexturesDelta,
} }
impl EguiGlium { impl EguiGlium {
@ -112,6 +115,8 @@ impl EguiGlium {
egui_ctx: Default::default(), egui_ctx: Default::default(),
egui_winit: egui_winit::State::new(display.gl_window().window()), egui_winit: egui_winit::State::new(display.gl_window().window()),
painter: crate::Painter::new(display), painter: crate::Painter::new(display),
shapes: Default::default(),
textures_delta: Default::default(),
} }
} }
@ -125,35 +130,45 @@ impl EguiGlium {
self.egui_winit.on_event(&self.egui_ctx, event) self.egui_winit.on_event(&self.egui_ctx, event)
} }
/// Returns `needs_repaint` and shapes to draw. /// Returns `true` if egui requests a repaint.
pub fn run( ///
&mut self, /// Call [`Self::paint`] later to paint.
display: &glium::Display, pub fn run(&mut self, display: &glium::Display, run_ui: impl FnMut(&egui::Context)) -> bool {
run_ui: impl FnMut(&egui::Context),
) -> (bool, Vec<egui::epaint::ClippedShape>) {
let raw_input = self let raw_input = self
.egui_winit .egui_winit
.take_egui_input(display.gl_window().window()); .take_egui_input(display.gl_window().window());
let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui);
let needs_repaint = egui_output.needs_repaint; let needs_repaint = egui_output.needs_repaint;
self.egui_winit let textures_delta = self.egui_winit.handle_output(
.handle_output(display.gl_window().window(), &self.egui_ctx, egui_output); display.gl_window().window(),
(needs_repaint, shapes) &self.egui_ctx,
egui_output,
);
self.shapes = shapes;
self.textures_delta.append(textures_delta);
needs_repaint
}
/// Paint the results of the last call to [`Self::run`].
pub fn paint<T: glium::Surface>(&mut self, display: &glium::Display, target: &mut T) {
let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set {
self.painter.set_texture(display, id, &image);
} }
pub fn paint<T: glium::Surface>(
&mut self,
display: &glium::Display,
target: &mut T,
shapes: Vec<egui::epaint::ClippedShape>,
) {
let clipped_meshes = self.egui_ctx.tessellate(shapes); let clipped_meshes = self.egui_ctx.tessellate(shapes);
self.painter.paint_meshes( self.painter.paint_meshes(
display, display,
target, target,
self.egui_ctx.pixels_per_point(), self.egui_ctx.pixels_per_point(),
clipped_meshes, clipped_meshes,
&self.egui_ctx.font_image(),
); );
for id in textures_delta.free.drain(..) {
self.painter.free_texture(id);
}
} }
} }

View File

@ -2,10 +2,8 @@
#![allow(semicolon_in_expressions_from_macros)] // glium::program! macro #![allow(semicolon_in_expressions_from_macros)] // glium::program! macro
use { use {
egui::{ ahash::AHashMap,
emath::Rect, egui::{emath::Rect, epaint::Mesh},
epaint::{Color32, Mesh},
},
glium::{ glium::{
implement_vertex, implement_vertex,
index::PrimitiveType, index::PrimitiveType,
@ -14,19 +12,17 @@ use {
uniform, uniform,
uniforms::{MagnifySamplerFilter, SamplerWrapFunction}, uniforms::{MagnifySamplerFilter, SamplerWrapFunction},
}, },
std::{collections::HashMap, rc::Rc}, std::rc::Rc,
}; };
pub struct Painter { pub struct Painter {
program: glium::Program, program: glium::Program,
egui_texture: Option<SrgbTexture2d>,
egui_texture_version: Option<u64>,
/// Index is the same as in [`egui::TextureId::User`]. textures: AHashMap<egui::TextureId, Rc<SrgbTexture2d>>,
user_textures: HashMap<u64, Rc<SrgbTexture2d>>,
#[cfg(feature = "epi")] #[cfg(feature = "epi")]
next_native_tex_id: u64, // TODO: 128-bit texture space? /// [`egui::TextureId::User`] index
next_native_tex_id: u64,
} }
impl Painter { impl Painter {
@ -54,40 +50,12 @@ impl Painter {
Painter { Painter {
program, program,
egui_texture: None, textures: Default::default(),
egui_texture_version: None,
user_textures: Default::default(),
#[cfg(feature = "epi")] #[cfg(feature = "epi")]
next_native_tex_id: 1 << 32, next_native_tex_id: 0,
} }
} }
pub fn upload_egui_texture(
&mut self,
facade: &dyn glium::backend::Facade,
font_image: &egui::FontImage,
) {
if self.egui_texture_version == Some(font_image.version) {
return; // No change
}
let pixels: Vec<Vec<(u8, u8, u8, u8)>> = font_image
.pixels
.chunks(font_image.width as usize)
.map(|row| {
row.iter()
.map(|&a| Color32::from_white_alpha(a).to_tuple())
.collect()
})
.collect();
let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap;
self.egui_texture =
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap());
self.egui_texture_version = Some(font_image.version);
}
/// Main entry-point for painting a frame. /// Main entry-point for painting a frame.
/// You should call `target.clear_color(..)` before /// You should call `target.clear_color(..)` before
/// and `target.finish()` after this. /// and `target.finish()` after this.
@ -97,10 +65,7 @@ impl Painter {
target: &mut T, target: &mut T,
pixels_per_point: f32, pixels_per_point: f32,
cipped_meshes: Vec<egui::ClippedMesh>, cipped_meshes: Vec<egui::ClippedMesh>,
font_image: &egui::FontImage,
) { ) {
self.upload_egui_texture(display, font_image);
for egui::ClippedMesh(clip_rect, mesh) in cipped_meshes { for egui::ClippedMesh(clip_rect, mesh) in cipped_meshes {
self.paint_mesh(target, display, pixels_per_point, clip_rect, &mesh); self.paint_mesh(target, display, pixels_per_point, clip_rect, &mesh);
} }
@ -118,7 +83,8 @@ impl Painter {
debug_assert!(mesh.is_valid()); debug_assert!(mesh.is_valid());
let vertex_buffer = { let vertex_buffer = {
#[derive(Copy, Clone)] #[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex { struct Vertex {
a_pos: [f32; 2], a_pos: [f32; 2],
a_tc: [f32; 2], a_tc: [f32; 2],
@ -126,18 +92,10 @@ impl Painter {
} }
implement_vertex!(Vertex, a_pos, a_tc, a_srgba); implement_vertex!(Vertex, a_pos, a_tc, a_srgba);
let vertices: Vec<Vertex> = mesh let vertices: &[Vertex] = bytemuck::cast_slice(&mesh.vertices);
.vertices
.iter()
.map(|v| Vertex {
a_pos: [v.pos.x, v.pos.y],
a_tc: [v.uv.x, v.uv.y],
a_srgba: v.color.to_array(),
})
.collect();
// TODO: we should probably reuse the `VertexBuffer` instead of allocating a new one each frame. // TODO: we should probably reuse the `VertexBuffer` instead of allocating a new one each frame.
glium::VertexBuffer::new(display, &vertices).unwrap() glium::VertexBuffer::new(display, vertices).unwrap()
}; };
// TODO: we should probably reuse the `IndexBuffer` instead of allocating a new one each frame. // TODO: we should probably reuse the `IndexBuffer` instead of allocating a new one each frame.
@ -223,41 +181,48 @@ impl Painter {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
#[cfg(feature = "epi")]
pub fn set_texture( pub fn set_texture(
&mut self, &mut self,
facade: &dyn glium::backend::Facade, facade: &dyn glium::backend::Facade,
tex_id: u64, tex_id: egui::TextureId,
image: &epi::Image, image: &egui::ImageData,
) { ) {
let pixels: Vec<(u8, u8, u8, u8)> = match image {
egui::ImageData::Color(image) => {
assert_eq!( assert_eq!(
image.size[0] * image.size[1], image.width() * image.height(),
image.pixels.len(), image.pixels.len(),
"Mismatch between texture size and texel count" "Mismatch between texture size and texel count"
); );
image.pixels.iter().map(|color| color.to_tuple()).collect()
let pixels: Vec<Vec<(u8, u8, u8, u8)>> = image }
.pixels egui::ImageData::Alpha(image) => {
.chunks(image.size[0] as usize) let gamma = 1.0;
.map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect()) image
.collect(); .srgba_pixels(gamma)
.map(|color| color.to_tuple())
.collect()
}
};
let glium_image = glium::texture::RawImage2d {
data: std::borrow::Cow::Owned(pixels),
width: image.width() as _,
height: image.height() as _,
format: glium::texture::ClientFormat::U8U8U8U8,
};
let format = texture::SrgbFormat::U8U8U8U8; let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap; let mipmaps = texture::MipmapsOption::NoMipmap;
let gl_texture = SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap(); let gl_texture = SrgbTexture2d::with_format(facade, glium_image, format, mipmaps).unwrap();
self.user_textures.insert(tex_id, gl_texture.into()); self.textures.insert(tex_id, gl_texture.into());
} }
pub fn free_texture(&mut self, tex_id: u64) { pub fn free_texture(&mut self, tex_id: egui::TextureId) {
self.user_textures.remove(&tex_id); self.textures.remove(&tex_id);
} }
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&SrgbTexture2d> { fn get_texture(&self, texture_id: egui::TextureId) -> Option<&SrgbTexture2d> {
match texture_id { self.textures.get(&texture_id).map(|rc| rc.as_ref())
egui::TextureId::Egui => self.egui_texture.as_ref(),
egui::TextureId::User(id) => self.user_textures.get(&id).map(|rc| rc.as_ref()),
}
} }
} }
@ -266,15 +231,13 @@ impl epi::NativeTexture for Painter {
type Texture = Rc<SrgbTexture2d>; type Texture = Rc<SrgbTexture2d>;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId { fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = self.next_native_tex_id; let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1; self.next_native_tex_id += 1;
self.user_textures.insert(id, native); self.textures.insert(id, native);
egui::TextureId::User(id as u64) id
} }
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) { fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let egui::TextureId::User(id) = id { self.textures.insert(id, replacing);
self.user_textures.insert(id, replacing);
}
} }
} }

View File

@ -3,9 +3,11 @@ All notable changes to the `egui_glow` integration will be noted in this file.
## Unreleased ## Unreleased
* `EguiGlow::run` no longer returns the shapes to paint, but stores them internally until you call `EguiGlow::paint` ([#1110](https://github.com/emilk/egui/pull/1110)).
* Added `set_texture_filter` method to `Painter` ((#1041)[https://github.com/emilk/egui/pull/1041]). * Added `set_texture_filter` method to `Painter` ((#1041)[https://github.com/emilk/egui/pull/1041]).
* Fix failure to run in Chrome ((#1092)[https://github.com/emilk/egui/pull/1092]). * Fix failure to run in Chrome ((#1092)[https://github.com/emilk/egui/pull/1092]).
## 0.16.0 - 2021-12-29 ## 0.16.0 - 2021-12-29
* Made winit/glutin an optional dependency ([#868](https://github.com/emilk/egui/pull/868)). * Made winit/glutin an optional dependency ([#868](https://github.com/emilk/egui/pull/868)).
* Simplified `EguiGlow` interface ([#871](https://github.com/emilk/egui/pull/871)). * Simplified `EguiGlow` interface ([#871](https://github.com/emilk/egui/pull/871)).

View File

@ -23,10 +23,13 @@ include = [
all-features = true all-features = true
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = ["single_threaded", "convert_bytemuck"] } egui = { version = "0.16.0", path = "../egui", default-features = false, features = [
"convert_bytemuck",
"single_threaded",
] }
epi = { version = "0.16.0", path = "../epi", optional = true }
bytemuck = "1.7" bytemuck = "1.7"
epi = { version = "0.16.0", path = "../epi", optional = true }
glow = "0.11" glow = "0.11"
memoffset = "0.6" memoffset = "0.6"

View File

@ -50,7 +50,7 @@ fn main() {
let mut redraw = || { let mut redraw = || {
let mut quit = false; let mut quit = false;
let (needs_repaint, shapes) = egui_glow.run(gl_window.window(), |egui_ctx| { let needs_repaint = egui_glow.run(gl_window.window(), |egui_ctx| {
egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| { egui::SidePanel::left("my_side_panel").show(egui_ctx, |ui| {
ui.heading("Hello World!"); ui.heading("Hello World!");
if ui.button("Quit").clicked() { if ui.button("Quit").clicked() {
@ -78,7 +78,7 @@ fn main() {
// draw things behind egui here // draw things behind egui here
egui_glow.paint(&gl_window, &gl, shapes); egui_glow.paint(&gl_window, &gl);
// draw things on top of egui here // draw things on top of egui here

View File

@ -82,11 +82,11 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
std::thread::sleep(std::time::Duration::from_millis(10)); std::thread::sleep(std::time::Duration::from_millis(10));
} }
let (needs_repaint, mut tex_allocation_data, shapes) = let (needs_repaint, mut textures_delta, shapes) =
integration.update(gl_window.window()); integration.update(gl_window.window());
let clipped_meshes = integration.egui_ctx.tessellate(shapes); let clipped_meshes = integration.egui_ctx.tessellate(shapes);
for (id, image) in tex_allocation_data.creations { for (id, image) in textures_delta.set {
painter.set_texture(&gl, id, &image); painter.set_texture(&gl, id, &image);
} }
@ -99,7 +99,6 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
gl.clear_color(color[0], color[1], color[2], color[3]); gl.clear_color(color[0], color[1], color[2], color[3]);
gl.clear(glow::COLOR_BUFFER_BIT); gl.clear(glow::COLOR_BUFFER_BIT);
} }
painter.upload_egui_texture(&gl, &integration.egui_ctx.font_image());
painter.paint_meshes( painter.paint_meshes(
&gl, &gl,
gl_window.window().inner_size().into(), gl_window.window().inner_size().into(),
@ -110,8 +109,8 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
gl_window.swap_buffers().unwrap(); gl_window.swap_buffers().unwrap();
} }
for id in tex_allocation_data.destructions.drain(..) { for id in textures_delta.free.drain(..) {
painter.free_texture(id); painter.free_texture(&gl, id);
} }
{ {

View File

@ -112,6 +112,9 @@ pub struct EguiGlow {
pub egui_ctx: egui::Context, pub egui_ctx: egui::Context,
pub egui_winit: egui_winit::State, pub egui_winit: egui_winit::State,
pub painter: crate::Painter, pub painter: crate::Painter,
shapes: Vec<egui::epaint::ClippedShape>,
textures_delta: egui::TexturesDelta,
} }
#[cfg(feature = "winit")] #[cfg(feature = "winit")]
@ -128,6 +131,8 @@ impl EguiGlow {
eprintln!("some error occurred in initializing painter\n{}", error); eprintln!("some error occurred in initializing painter\n{}", error);
}) })
.unwrap(), .unwrap(),
shapes: Default::default(),
textures_delta: Default::default(),
} }
} }
@ -141,36 +146,51 @@ impl EguiGlow {
self.egui_winit.on_event(&self.egui_ctx, event) self.egui_winit.on_event(&self.egui_ctx, event)
} }
/// Returns `needs_repaint` and shapes to draw. /// Returns `true` if egui requests a repaint.
///
/// Call [`Self::paint`] later to paint.
pub fn run( pub fn run(
&mut self, &mut self,
window: &glutin::window::Window, window: &glutin::window::Window,
run_ui: impl FnMut(&egui::Context), run_ui: impl FnMut(&egui::Context),
) -> (bool, Vec<egui::epaint::ClippedShape>) { ) -> bool {
let raw_input = self.egui_winit.take_egui_input(window); let raw_input = self.egui_winit.take_egui_input(window);
let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui);
let needs_repaint = egui_output.needs_repaint; let needs_repaint = egui_output.needs_repaint;
self.egui_winit let textures_delta = self
.egui_winit
.handle_output(window, &self.egui_ctx, egui_output); .handle_output(window, &self.egui_ctx, egui_output);
(needs_repaint, shapes)
self.shapes = shapes;
self.textures_delta.append(textures_delta);
needs_repaint
} }
/// Paint the results of the last call to [`Self::run`].
pub fn paint( pub fn paint(
&mut self, &mut self,
gl_window: &glutin::WindowedContext<glutin::PossiblyCurrent>, gl_window: &glutin::WindowedContext<glutin::PossiblyCurrent>,
gl: &glow::Context, gl: &glow::Context,
shapes: Vec<egui::epaint::ClippedShape>,
) { ) {
let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set {
self.painter.set_texture(gl, id, &image);
}
let clipped_meshes = self.egui_ctx.tessellate(shapes); let clipped_meshes = self.egui_ctx.tessellate(shapes);
let dimensions: [u32; 2] = gl_window.window().inner_size().into(); let dimensions: [u32; 2] = gl_window.window().inner_size().into();
self.painter
.upload_egui_texture(gl, &self.egui_ctx.font_image());
self.painter.paint_meshes( self.painter.paint_meshes(
gl, gl,
dimensions, dimensions,
self.egui_ctx.pixels_per_point(), self.egui_ctx.pixels_per_point(),
clipped_meshes, clipped_meshes,
); );
for id in textures_delta.free.drain(..) {
self.painter.free_texture(gl, id);
}
} }
/// Call to release the allocated graphics resources. /// Call to release the allocated graphics resources.

View File

@ -86,10 +86,6 @@ pub fn check_for_gl_error(gl: &glow::Context, context: &str) {
} }
} }
pub(crate) 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>())
}
pub(crate) fn glow_print(s: impl std::fmt::Display) { pub(crate) fn glow_print(s: impl std::fmt::Display) {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
web_sys::console::log_1(&format!("egui_glow: {}", s).into()); web_sys::console::log_1(&format!("egui_glow: {}", s).into());

View File

@ -2,7 +2,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use bytemuck::cast_slice;
use egui::{ use egui::{
emath::Rect, emath::Rect,
epaint::{Color32, Mesh, Vertex}, epaint::{Color32, Mesh, Vertex},
@ -11,7 +10,7 @@ use glow::HasContext;
use memoffset::offset_of; use memoffset::offset_of;
use crate::misc_util::{ use crate::misc_util::{
as_u8_slice, check_for_gl_error, compile_shader, glow_print, link_program, srgb_texture2d, check_for_gl_error, compile_shader, glow_print, link_program, srgb_texture2d,
}; };
use crate::post_process::PostProcess; use crate::post_process::PostProcess;
use crate::shader_version::ShaderVersion; use crate::shader_version::ShaderVersion;
@ -30,8 +29,6 @@ pub struct Painter {
program: glow::Program, program: glow::Program,
u_screen_size: glow::UniformLocation, u_screen_size: glow::UniformLocation,
u_sampler: glow::UniformLocation, u_sampler: glow::UniformLocation,
egui_texture: Option<glow::Texture>,
egui_texture_version: Option<u64>,
is_webgl_1: bool, is_webgl_1: bool,
is_embedded: bool, is_embedded: bool,
vertex_array: crate::misc_util::VAO, vertex_array: crate::misc_util::VAO,
@ -42,8 +39,7 @@ pub struct Painter {
vertex_buffer: glow::Buffer, vertex_buffer: glow::Buffer,
element_array_buffer: glow::Buffer, element_array_buffer: glow::Buffer,
/// Index is the same as in [`egui::TextureId::User`]. textures: HashMap<egui::TextureId, glow::Texture>,
user_textures: HashMap<u64, glow::Texture>,
#[cfg(feature = "epi")] #[cfg(feature = "epi")]
next_native_tex_id: u64, // TODO: 128-bit texture space? next_native_tex_id: u64, // TODO: 128-bit texture space?
@ -212,8 +208,6 @@ impl Painter {
program, program,
u_screen_size, u_screen_size,
u_sampler, u_sampler,
egui_texture: None,
egui_texture_version: None,
is_webgl_1, is_webgl_1,
is_embedded: matches!(shader_version, ShaderVersion::Es100 | ShaderVersion::Es300), is_embedded: matches!(shader_version, ShaderVersion::Es100 | ShaderVersion::Es300),
vertex_array, vertex_array,
@ -222,7 +216,7 @@ impl Painter {
post_process, post_process,
vertex_buffer, vertex_buffer,
element_array_buffer, element_array_buffer,
user_textures: Default::default(), textures: Default::default(),
#[cfg(feature = "epi")] #[cfg(feature = "epi")]
next_native_tex_id: 1 << 32, next_native_tex_id: 1 << 32,
textures_to_destroy: Vec::new(), textures_to_destroy: Vec::new(),
@ -231,41 +225,6 @@ impl Painter {
} }
} }
pub fn upload_egui_texture(&mut self, gl: &glow::Context, font_image: &egui::FontImage) {
self.assert_not_destroyed();
if self.egui_texture_version == Some(font_image.version) {
return; // No change
}
let gamma = if self.is_embedded && self.post_process.is_none() {
1.0 / 2.2
} else {
1.0
};
let pixels: Vec<u8> = font_image
.srgba_pixels(gamma)
.flat_map(|a| Vec::from(a.to_array()))
.collect();
if let Some(old_tex) = std::mem::replace(
&mut self.egui_texture,
Some(srgb_texture2d(
gl,
self.is_webgl_1,
self.srgb_support,
self.texture_filter,
&pixels,
font_image.width,
font_image.height,
)),
) {
unsafe {
gl.delete_texture(old_tex);
}
}
self.egui_texture_version = Some(font_image.version);
}
unsafe fn prepare_painting( unsafe fn prepare_painting(
&mut self, &mut self,
[width_in_pixels, height_in_pixels]: [u32; 2], [width_in_pixels, height_in_pixels]: [u32; 2],
@ -370,14 +329,14 @@ impl Painter {
gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer)); gl.bind_buffer(glow::ARRAY_BUFFER, Some(self.vertex_buffer));
gl.buffer_data_u8_slice( gl.buffer_data_u8_slice(
glow::ARRAY_BUFFER, glow::ARRAY_BUFFER,
as_u8_slice(mesh.vertices.as_slice()), bytemuck::cast_slice(&mesh.vertices),
glow::STREAM_DRAW, glow::STREAM_DRAW,
); );
gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.element_array_buffer)); gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.element_array_buffer));
gl.buffer_data_u8_slice( gl.buffer_data_u8_slice(
glow::ELEMENT_ARRAY_BUFFER, glow::ELEMENT_ARRAY_BUFFER,
as_u8_slice(mesh.indices.as_slice()), bytemuck::cast_slice(&mesh.indices),
glow::STREAM_DRAW, glow::STREAM_DRAW,
); );
@ -425,19 +384,25 @@ impl Painter {
// ------------------------------------------------------------------------ // ------------------------------------------------------------------------
#[cfg(feature = "epi")] pub fn set_texture(
pub fn set_texture(&mut self, gl: &glow::Context, tex_id: u64, image: &epi::Image) { &mut self,
gl: &glow::Context,
tex_id: egui::TextureId,
image: &egui::ImageData,
) {
self.assert_not_destroyed(); self.assert_not_destroyed();
let gl_texture = match image {
egui::ImageData::Color(image) => {
assert_eq!( assert_eq!(
image.size[0] * image.size[1], image.width() * image.height(),
image.pixels.len(), image.pixels.len(),
"Mismatch between texture size and texel count" "Mismatch between texture size and texel count"
); );
let data: &[u8] = cast_slice(image.pixels.as_ref()); let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref());
let gl_texture = srgb_texture2d( srgb_texture2d(
gl, gl,
self.is_webgl_1, self.is_webgl_1,
self.srgb_support, self.srgb_support,
@ -445,32 +410,49 @@ impl Painter {
data, data,
image.size[0], image.size[0],
image.size[1], image.size[1],
); )
}
egui::ImageData::Alpha(image) => {
let gamma = if self.is_embedded && self.post_process.is_none() {
1.0 / 2.2
} else {
1.0
};
let data: Vec<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
if let Some(old_tex) = self.user_textures.insert(tex_id, gl_texture) { srgb_texture2d(
self.textures_to_destroy.push(old_tex); gl,
self.is_webgl_1,
self.srgb_support,
self.texture_filter,
&data,
image.size[0],
image.size[1],
)
}
};
if let Some(old_tex) = self.textures.insert(tex_id, gl_texture) {
unsafe { gl.delete_texture(old_tex) };
} }
} }
pub fn free_texture(&mut self, tex_id: u64) { pub fn free_texture(&mut self, gl: &glow::Context, tex_id: egui::TextureId) {
self.user_textures.remove(&tex_id); if let Some(old_tex) = self.textures.remove(&tex_id) {
unsafe { gl.delete_texture(old_tex) };
}
} }
fn get_texture(&self, texture_id: egui::TextureId) -> Option<glow::Texture> { fn get_texture(&self, texture_id: egui::TextureId) -> Option<glow::Texture> {
self.assert_not_destroyed(); self.textures.get(&texture_id).copied()
match texture_id {
egui::TextureId::Egui => self.egui_texture,
egui::TextureId::User(id) => self.user_textures.get(&id).copied(),
}
} }
unsafe fn destroy_gl(&self, gl: &glow::Context) { unsafe fn destroy_gl(&self, gl: &glow::Context) {
gl.delete_program(self.program); gl.delete_program(self.program);
if let Some(tex) = self.egui_texture { for tex in self.textures.values() {
gl.delete_texture(tex);
}
for tex in self.user_textures.values() {
gl.delete_texture(*tex); gl.delete_texture(*tex);
} }
gl.delete_buffer(self.vertex_buffer); gl.delete_buffer(self.vertex_buffer);
@ -533,20 +515,15 @@ impl epi::NativeTexture for Painter {
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId { fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
self.assert_not_destroyed(); self.assert_not_destroyed();
let id = egui::TextureId::User(self.next_native_tex_id);
let id = self.next_native_tex_id;
self.next_native_tex_id += 1; self.next_native_tex_id += 1;
self.textures.insert(id, native);
self.user_textures.insert(id, native); id
egui::TextureId::User(id as u64)
} }
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) { fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let egui::TextureId::User(id) = id { if let Some(old_tex) = self.textures.insert(id, replacing) {
if let Some(old_tex) = self.user_textures.insert(id, replacing) {
self.textures_to_destroy.push(old_tex); self.textures_to_destroy.push(old_tex);
} }
} }
}
} }

View File

@ -116,7 +116,7 @@ impl PostProcess {
gl.bind_buffer(glow::ARRAY_BUFFER, Some(pos_buffer)); gl.bind_buffer(glow::ARRAY_BUFFER, Some(pos_buffer));
gl.buffer_data_u8_slice( gl.buffer_data_u8_slice(
glow::ARRAY_BUFFER, glow::ARRAY_BUFFER,
crate::misc_util::as_u8_slice(&positions), bytemuck::cast_slice(&positions),
glow::STATIC_DRAW, glow::STATIC_DRAW,
); );

View File

@ -27,10 +27,13 @@ crate-type = ["cdylib", "rlib"]
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false, features = [ egui = { version = "0.16.0", path = "../egui", default-features = false, features = [
"convert_bytemuck",
"single_threaded", "single_threaded",
] } ] }
egui_glow = { version = "0.16.0",path = "../egui_glow", default-features = false, optional = true } egui_glow = { version = "0.16.0",path = "../egui_glow", default-features = false, optional = true }
epi = { version = "0.16.0", path = "../epi" } epi = { version = "0.16.0", path = "../epi" }
bytemuck = "1.7"
js-sys = "0.3" js-sys = "0.3"
ron = { version = "0.7", optional = true } ron = { version = "0.7", optional = true }
serde = { version = "1", optional = true } serde = { version = "1", optional = true }

View File

@ -1,5 +1,6 @@
use crate::*; use crate::*;
use egui::TexturesDelta;
pub use egui::{pos2, Color32}; pub use egui::{pos2, Color32};
// ---------------------------------------------------------------------------- // ----------------------------------------------------------------------------
@ -92,7 +93,7 @@ pub struct AppRunner {
screen_reader: crate::screen_reader::ScreenReader, screen_reader: crate::screen_reader::ScreenReader,
pub(crate) text_cursor_pos: Option<egui::Pos2>, pub(crate) text_cursor_pos: Option<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool, pub(crate) mutable_text_under_cursor: bool,
pending_texture_destructions: Vec<u64>, textures_delta: TexturesDelta,
} }
impl AppRunner { impl AppRunner {
@ -139,7 +140,7 @@ impl AppRunner {
screen_reader: Default::default(), screen_reader: Default::default(),
text_cursor_pos: None, text_cursor_pos: None,
mutable_text_under_cursor: false, mutable_text_under_cursor: false,
pending_texture_destructions: Default::default(), textures_delta: Default::default(),
}; };
{ {
@ -183,7 +184,10 @@ impl AppRunner {
Ok(()) Ok(())
} }
pub fn logic(&mut self) -> Result<(egui::Output, Vec<egui::ClippedMesh>), JsValue> { /// Returns `true` if egui requests a repaint.
///
/// Call [`Self::paint`] later to paint
pub fn logic(&mut self) -> Result<(bool, Vec<egui::ClippedMesh>), JsValue> {
let frame_start = now_sec(); let frame_start = now_sec();
resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points()); resize_canvas_to_screen_size(self.canvas_id(), self.app.max_size_points());
@ -195,7 +199,9 @@ impl AppRunner {
}); });
let clipped_meshes = self.egui_ctx.tessellate(shapes); let clipped_meshes = self.egui_ctx.tessellate(shapes);
self.handle_egui_output(&egui_output); let needs_repaint = egui_output.needs_repaint;
let textures_delta = self.handle_egui_output(egui_output);
self.textures_delta.append(textures_delta);
{ {
let app_output = self.frame.take_app_output(); let app_output = self.frame.take_app_output();
@ -205,32 +211,32 @@ impl AppRunner {
window_title: _, // TODO: change title of window window_title: _, // TODO: change title of window
decorated: _, // Can't toggle decorations decorated: _, // Can't toggle decorations
drag_window: _, // Can't be dragged drag_window: _, // Can't be dragged
tex_allocation_data,
} = app_output; } = app_output;
for (id, image) in tex_allocation_data.creations {
self.painter.set_texture(id, image);
}
self.pending_texture_destructions = tex_allocation_data.destructions;
} }
self.frame.lock().info.cpu_usage = Some((now_sec() - frame_start) as f32); self.frame.lock().info.cpu_usage = Some((now_sec() - frame_start) as f32);
Ok((egui_output, clipped_meshes)) Ok((needs_repaint, clipped_meshes))
} }
/// Paint the results of the last call to [`Self::logic`].
pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> { pub fn paint(&mut self, clipped_meshes: Vec<egui::ClippedMesh>) -> Result<(), JsValue> {
self.painter let textures_delta = std::mem::take(&mut self.textures_delta);
.upload_egui_texture(&self.egui_ctx.font_image()); for (id, image) in textures_delta.set {
self.painter.set_texture(id, image);
}
self.painter.clear(self.app.clear_color()); self.painter.clear(self.app.clear_color());
self.painter self.painter
.paint_meshes(clipped_meshes, self.egui_ctx.pixels_per_point())?; .paint_meshes(clipped_meshes, self.egui_ctx.pixels_per_point())?;
for id in self.pending_texture_destructions.drain(..) {
for id in textures_delta.free {
self.painter.free_texture(id); self.painter.free_texture(id);
} }
Ok(()) Ok(())
} }
fn handle_egui_output(&mut self, output: &egui::Output) { fn handle_egui_output(&mut self, output: egui::Output) -> egui::TexturesDelta {
if self.egui_ctx.memory().options.screen_reader { if self.egui_ctx.memory().options.screen_reader {
self.screen_reader.speak(&output.events_description()); self.screen_reader.speak(&output.events_description());
} }
@ -243,27 +249,30 @@ impl AppRunner {
events: _, // already handled events: _, // already handled
mutable_text_under_cursor, mutable_text_under_cursor,
text_cursor_pos, text_cursor_pos,
textures_delta,
} = output; } = output;
set_cursor_icon(*cursor_icon); set_cursor_icon(cursor_icon);
if let Some(open) = open_url { if let Some(open) = open_url {
crate::open_url(&open.url, open.new_tab); crate::open_url(&open.url, open.new_tab);
} }
#[cfg(web_sys_unstable_apis)] #[cfg(web_sys_unstable_apis)]
if !copied_text.is_empty() { if !copied_text.is_empty() {
set_clipboard_text(copied_text); set_clipboard_text(&copied_text);
} }
#[cfg(not(web_sys_unstable_apis))] #[cfg(not(web_sys_unstable_apis))]
let _ = copied_text; let _ = copied_text;
self.mutable_text_under_cursor = *mutable_text_under_cursor; self.mutable_text_under_cursor = mutable_text_under_cursor;
if &self.text_cursor_pos != text_cursor_pos { if self.text_cursor_pos != text_cursor_pos {
move_text_cursor(text_cursor_pos, self.canvas_id()); move_text_cursor(text_cursor_pos, self.canvas_id());
self.text_cursor_pos = *text_cursor_pos; self.text_cursor_pos = text_cursor_pos;
} }
textures_delta
} }
} }

View File

@ -1,5 +1,5 @@
use crate::{canvas_element_or_die, console_error}; use crate::{canvas_element_or_die, console_error};
use egui::{ClippedMesh, FontImage, Rgba}; use egui::{ClippedMesh, Rgba};
use egui_glow::glow; use egui_glow::glow;
use wasm_bindgen::JsCast; use wasm_bindgen::JsCast;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
@ -40,12 +40,12 @@ impl WrappedGlowPainter {
} }
impl crate::Painter for WrappedGlowPainter { impl crate::Painter for WrappedGlowPainter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image) { fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) {
self.painter.set_texture(&self.glow_ctx, tex_id, &image); self.painter.set_texture(&self.glow_ctx, tex_id, &image);
} }
fn free_texture(&mut self, tex_id: u64) { fn free_texture(&mut self, tex_id: egui::TextureId) {
self.painter.free_texture(tex_id); self.painter.free_texture(&self.glow_ctx, tex_id);
} }
fn debug_info(&self) -> String { fn debug_info(&self) -> String {
@ -60,10 +60,6 @@ impl crate::Painter for WrappedGlowPainter {
&self.canvas_id &self.canvas_id
} }
fn upload_egui_texture(&mut self, font_image: &FontImage) {
self.painter.upload_egui_texture(&self.glow_ctx, font_image)
}
fn clear(&mut self, clear_color: Rgba) { fn clear(&mut self, clear_color: Rgba) {
let canvas_dimension = [self.canvas.width(), self.canvas.height()]; let canvas_dimension = [self.canvas.width(), self.canvas.height()];
egui_glow::painter::clear(&self.glow_ctx, canvas_dimension, clear_color) egui_glow::painter::clear(&self.glow_ctx, canvas_dimension, clear_color)

View File

@ -482,9 +482,9 @@ fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> {
fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> {
let mut runner_lock = runner_ref.0.lock(); let mut runner_lock = runner_ref.0.lock();
if runner_lock.needs_repaint.fetch_and_clear() { if runner_lock.needs_repaint.fetch_and_clear() {
let (output, clipped_meshes) = runner_lock.logic()?; let (needs_repaint, clipped_meshes) = runner_lock.logic()?;
runner_lock.paint(clipped_meshes)?; runner_lock.paint(clipped_meshes)?;
if output.needs_repaint { if needs_repaint {
runner_lock.needs_repaint.set_true(); runner_lock.needs_repaint.set_true();
} }
runner_lock.auto_save(); runner_lock.auto_save();
@ -1214,7 +1214,7 @@ fn is_mobile() -> Option<bool> {
// candidate window moves following text element (agent), // candidate window moves following text element (agent),
// so it appears that the IME candidate window moves with text cursor. // so it appears that the IME candidate window moves with text cursor.
// On mobile devices, there is no need to do that. // On mobile devices, there is no need to do that.
fn move_text_cursor(cursor: &Option<egui::Pos2>, canvas_id: &str) -> Option<()> { fn move_text_cursor(cursor: Option<egui::Pos2>, canvas_id: &str) -> Option<()> {
let style = text_agent().style(); let style = text_agent().style();
// Note: movint agent on mobile devices will lead to unpredictable scroll. // Note: movint agent on mobile devices will lead to unpredictable scroll.
if is_mobile() == Some(false) { if is_mobile() == Some(false) {

View File

@ -1,17 +1,15 @@
use wasm_bindgen::prelude::JsValue; use wasm_bindgen::prelude::JsValue;
pub trait Painter { pub trait Painter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image); fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData);
fn free_texture(&mut self, tex_id: u64); fn free_texture(&mut self, tex_id: egui::TextureId);
fn debug_info(&self) -> String; fn debug_info(&self) -> String;
/// id of the canvas html element containing the rendering /// id of the canvas html element containing the rendering
fn canvas_id(&self) -> &str; fn canvas_id(&self) -> &str;
fn upload_egui_texture(&mut self, font_image: &egui::FontImage);
fn clear(&mut self, clear_color: egui::Rgba); fn clear(&mut self, clear_color: egui::Rgba);
fn paint_meshes( fn paint_meshes(

View File

@ -9,10 +9,7 @@ use {
}, },
}; };
use egui::{ use egui::{emath::vec2, epaint::Color32};
emath::vec2,
epaint::{Color32, FontImage},
};
type Gl = WebGlRenderingContext; type Gl = WebGlRenderingContext;
@ -28,13 +25,8 @@ pub struct WebGlPainter {
texture_format: u32, texture_format: u32,
post_process: Option<PostProcess>, post_process: Option<PostProcess>,
egui_texture: WebGlTexture, textures: HashMap<egui::TextureId, WebGlTexture>,
egui_texture_version: Option<u64>, next_native_tex_id: u64,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, WebGlTexture>,
next_native_tex_id: u64, // TODO: 128-bit texture space?
} }
impl WebGlPainter { impl WebGlPainter {
@ -101,18 +93,13 @@ impl WebGlPainter {
color_buffer, color_buffer,
texture_format, texture_format,
post_process, post_process,
egui_texture, textures: Default::default(),
egui_texture_version: None,
user_textures: Default::default(),
next_native_tex_id: 1 << 32, next_native_tex_id: 1 << 32,
}) })
} }
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> { fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> {
match texture_id { self.textures.get(&texture_id)
egui::TextureId::Egui => Some(&self.egui_texture),
egui::TextureId::User(id) => self.user_textures.get(&id),
}
} }
fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> { fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> {
@ -234,44 +221,8 @@ impl WebGlPainter {
Ok(()) Ok(())
} }
}
impl epi::NativeTexture for WebGlPainter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = self.next_native_tex_id;
self.next_native_tex_id += 1;
self.user_textures.insert(id, native);
egui::TextureId::User(id as u64)
}
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let egui::TextureId::User(id) = id {
if let Some(user_texture) = self.user_textures.get_mut(&id) {
*user_texture = replacing;
}
}
}
}
impl crate::Painter for WebGlPainter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image) {
assert_eq!(
image.size[0] * image.size[1],
image.pixels.len(),
"Mismatch between texture size and texel count"
);
// TODO: optimize
let mut pixels: Vec<u8> = Vec::with_capacity(image.pixels.len() * 4);
for srgba in image.pixels {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) {
let gl = &self.gl; let gl = &self.gl;
let gl_texture = gl.create_texture().unwrap(); let gl_texture = gl.create_texture().unwrap();
gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture));
@ -291,20 +242,64 @@ impl crate::Painter for WebGlPainter {
Gl::TEXTURE_2D, Gl::TEXTURE_2D,
level, level,
internal_format as _, internal_format as _,
image.size[0] as _, size[0] as _,
image.size[1] as _, size[1] as _,
border, border,
src_format, src_format,
src_type, src_type,
Some(&pixels), Some(pixels),
) )
.unwrap(); .unwrap();
self.user_textures.insert(tex_id, gl_texture); self.textures.insert(tex_id, gl_texture);
}
}
impl epi::NativeTexture for WebGlPainter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, texture: Self::Texture) -> egui::TextureId {
let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1;
self.textures.insert(id, texture);
id
} }
fn free_texture(&mut self, tex_id: u64) { fn replace_native_texture(&mut self, id: egui::TextureId, texture: Self::Texture) {
self.user_textures.remove(&tex_id); self.textures.insert(id, texture);
}
}
impl crate::Painter for WebGlPainter {
fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) {
match image {
egui::ImageData::Color(image) => {
assert_eq!(
image.width() * image.height(),
image.pixels.len(),
"Mismatch between texture size and texel count"
);
let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref());
self.set_texture_rgba(tex_id, image.size, data);
}
egui::ImageData::Alpha(image) => {
let gamma = if self.post_process.is_none() {
1.0 / 2.2 // HACK due to non-linear framebuffer blending.
} else {
1.0 // post process enables linear blending
};
let data: Vec<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
self.set_texture_rgba(tex_id, image.size, &data);
}
};
}
fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id);
} }
fn debug_info(&self) -> String { fn debug_info(&self) -> String {
@ -323,48 +318,6 @@ impl crate::Painter for WebGlPainter {
&self.canvas_id &self.canvas_id
} }
fn upload_egui_texture(&mut self, font_image: &FontImage) {
if self.egui_texture_version == Some(font_image.version) {
return; // No change
}
let gamma = if self.post_process.is_none() {
1.0 / 2.2 // HACK due to non-linear framebuffer blending.
} else {
1.0 // post process enables linear blending
};
let mut pixels: Vec<u8> = Vec::with_capacity(font_image.pixels.len() * 4);
for srgba in font_image.srgba_pixels(gamma) {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
let gl = &self.gl;
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture));
let level = 0;
let internal_format = self.texture_format;
let border = 0;
let src_format = self.texture_format;
let src_type = Gl::UNSIGNED_BYTE;
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
level,
internal_format as i32,
font_image.width as i32,
font_image.height as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
self.egui_texture_version = Some(font_image.version);
}
fn clear(&mut self, clear_color: egui::Rgba) { fn clear(&mut self, clear_color: egui::Rgba) {
let gl = &self.gl; let gl = &self.gl;

View File

@ -10,10 +10,7 @@ use {
}, },
}; };
use egui::{ use egui::{emath::vec2, epaint::Color32};
emath::vec2,
epaint::{Color32, FontImage},
};
type Gl = WebGl2RenderingContext; type Gl = WebGl2RenderingContext;
@ -28,13 +25,8 @@ pub struct WebGl2Painter {
color_buffer: WebGlBuffer, color_buffer: WebGlBuffer,
post_process: PostProcess, post_process: PostProcess,
egui_texture: WebGlTexture, textures: HashMap<egui::TextureId, WebGlTexture>,
egui_texture_version: Option<u64>, next_native_tex_id: u64,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, WebGlTexture>,
next_native_tex_id: u64, // TODO: 128-bit texture space?
} }
impl WebGl2Painter { impl WebGl2Painter {
@ -85,18 +77,13 @@ impl WebGl2Painter {
tc_buffer, tc_buffer,
color_buffer, color_buffer,
post_process, post_process,
egui_texture, textures: Default::default(),
egui_texture_version: None,
user_textures: Default::default(),
next_native_tex_id: 1 << 32, next_native_tex_id: 1 << 32,
}) })
} }
fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> { fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> {
match texture_id { self.textures.get(&texture_id)
egui::TextureId::Egui => Some(&self.egui_texture),
egui::TextureId::User(id) => self.user_textures.get(&id),
}
} }
fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> { fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> {
@ -218,44 +205,8 @@ impl WebGl2Painter {
Ok(()) Ok(())
} }
}
impl epi::NativeTexture for WebGl2Painter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = self.next_native_tex_id;
self.next_native_tex_id += 1;
self.user_textures.insert(id, native);
egui::TextureId::User(id as u64)
}
fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) {
if let egui::TextureId::User(id) = id {
if let Some(user_texture) = self.user_textures.get_mut(&id) {
*user_texture = replacing;
}
}
}
}
impl crate::Painter for WebGl2Painter {
fn set_texture(&mut self, tex_id: u64, image: epi::Image) {
assert_eq!(
image.size[0] * image.size[1],
image.pixels.len(),
"Mismatch between texture size and texel count"
);
// TODO: optimize
let mut pixels: Vec<u8> = Vec::with_capacity(image.pixels.len() * 4);
for srgba in image.pixels {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) {
let gl = &self.gl; let gl = &self.gl;
let gl_texture = gl.create_texture().unwrap(); let gl_texture = gl.create_texture().unwrap();
gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture));
@ -271,24 +222,65 @@ impl crate::Painter for WebGl2Painter {
let border = 0; let border = 0;
let src_format = Gl::RGBA; let src_format = Gl::RGBA;
let src_type = Gl::UNSIGNED_BYTE; let src_type = Gl::UNSIGNED_BYTE;
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array( gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D, Gl::TEXTURE_2D,
level, level,
internal_format as _, internal_format as i32,
image.size[0] as _, size[0] as i32,
image.size[1] as _, size[1] as i32,
border, border,
src_format, src_format,
src_type, src_type,
Some(&pixels), Some(pixels),
) )
.unwrap(); .unwrap();
self.user_textures.insert(tex_id, gl_texture); self.textures.insert(tex_id, gl_texture);
}
}
impl epi::NativeTexture for WebGl2Painter {
type Texture = WebGlTexture;
fn register_native_texture(&mut self, native: Self::Texture) -> egui::TextureId {
let id = egui::TextureId::User(self.next_native_tex_id);
self.next_native_tex_id += 1;
self.textures.insert(id, native);
id
} }
fn free_texture(&mut self, tex_id: u64) { fn replace_native_texture(&mut self, id: egui::TextureId, native: Self::Texture) {
self.user_textures.remove(&tex_id); self.textures.insert(id, native);
}
}
impl crate::Painter for WebGl2Painter {
fn set_texture(&mut self, tex_id: egui::TextureId, image: egui::ImageData) {
match image {
egui::ImageData::Color(image) => {
assert_eq!(
image.width() * image.height(),
image.pixels.len(),
"Mismatch between texture size and texel count"
);
let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref());
self.set_texture_rgba(tex_id, image.size, data);
}
egui::ImageData::Alpha(image) => {
let gamma = 1.0;
let data: Vec<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
self.set_texture_rgba(tex_id, image.size, &data);
}
};
}
fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id);
} }
fn debug_info(&self) -> String { fn debug_info(&self) -> String {
@ -307,44 +299,6 @@ impl crate::Painter for WebGl2Painter {
&self.canvas_id &self.canvas_id
} }
fn upload_egui_texture(&mut self, font_image: &FontImage) {
if self.egui_texture_version == Some(font_image.version) {
return; // No change
}
let mut pixels: Vec<u8> = Vec::with_capacity(font_image.pixels.len() * 4);
for srgba in font_image.srgba_pixels(1.0) {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
let gl = &self.gl;
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture));
let level = 0;
let internal_format = Gl::SRGB8_ALPHA8;
let border = 0;
let src_format = Gl::RGBA;
let src_type = Gl::UNSIGNED_BYTE;
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
level,
internal_format as i32,
font_image.width as i32,
font_image.height as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
self.egui_texture_version = Some(font_image.version);
}
fn clear(&mut self, clear_color: egui::Rgba) { fn clear(&mut self, clear_color: egui::Rgba) {
let gl = &self.gl; let gl = &self.gl;

View File

@ -47,7 +47,7 @@ impl Align {
} }
} }
/// ``` rust /// ```
/// assert_eq!(emath::Align::Min.align_size_within_range(2.0, 10.0..=20.0), 10.0..=12.0); /// assert_eq!(emath::Align::Min.align_size_within_range(2.0, 10.0..=20.0), 10.0..=12.0);
/// assert_eq!(emath::Align::Center.align_size_within_range(2.0, 10.0..=20.0), 14.0..=16.0); /// assert_eq!(emath::Align::Center.align_size_within_range(2.0, 10.0..=20.0), 14.0..=16.0);
/// assert_eq!(emath::Align::Max.align_size_within_range(2.0, 10.0..=20.0), 18.0..=20.0); /// assert_eq!(emath::Align::Max.align_size_within_range(2.0, 10.0..=20.0), 18.0..=20.0);

View File

@ -4,8 +4,9 @@ All notable changes to the epaint crate will be documented in this file.
## Unreleased ## Unreleased
* Added `Shape::dashed_line_many` ([#1027](https://github.com/emilk/egui/pull/1027)). * Added `Shape::dashed_line_many` ([#1027](https://github.com/emilk/egui/pull/1027)).
* Add `ImageData` and `TextureManager` for loading images into textures ([#1110](https://github.com/emilk/egui/pull/1110)).
## 0.16.0 - 2021-12-29 ## 0.16.0 - 2021-12-29
* Anti-alias path ends ([#893](https://github.com/emilk/egui/pull/893)). * Anti-alias path ends ([#893](https://github.com/emilk/egui/pull/893)).

241
epaint/src/image.rs Normal file
View File

@ -0,0 +1,241 @@
use crate::Color32;
/// An image stored in RAM.
///
/// To load an image file, see [`ColorImage::from_rgba_unmultiplied`].
///
/// In order to paint the image on screen, you first need to convert it to
///
/// See also: [`ColorImage`], [`AlphaImage`].
#[derive(Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum ImageData {
/// RGBA image.
Color(ColorImage),
/// Used for the font texture.
Alpha(AlphaImage),
}
impl ImageData {
pub fn size(&self) -> [usize; 2] {
match self {
Self::Color(image) => image.size,
Self::Alpha(image) => image.size,
}
}
pub fn width(&self) -> usize {
self.size()[0]
}
pub fn height(&self) -> usize {
self.size()[1]
}
pub fn bytes_per_pixel(&self) -> usize {
match self {
Self::Color(_) => 4,
Self::Alpha(_) => 1,
}
}
}
// ----------------------------------------------------------------------------
/// A 2D RGBA color image in RAM.
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct ColorImage {
/// width, height.
pub size: [usize; 2],
/// The pixels, row by row, from top to bottom.
pub pixels: Vec<Color32>,
}
impl ColorImage {
/// Create an image filled with the given color.
pub fn new(size: [usize; 2], color: Color32) -> Self {
Self {
size,
pixels: vec![color; size[0] * size[1]],
}
}
/// Create an `Image` from flat un-multiplied RGBA data.
///
/// This is usually what you want to use after having loaded an image file.
///
/// Panics if `size[0] * size[1] * 4 != rgba.len()`.
///
/// ## Example using the [`image`](crates.io/crates/image) crate:
/// ``` ignore
/// fn load_image_from_path(path: &std::path::Path) -> Result<egui::ColorImage, image::ImageError> {
/// use image::GenericImageView as _;
/// let image = image::io::Reader::open(path)?.decode()?;
/// let size = [image.width() as _, image.height() as _];
/// let image_buffer = image.to_rgba8();
/// let pixels = image_buffer.as_flat_samples();
/// Ok(egui::ColorImage::from_rgba_unmultiplied(
/// size,
/// pixels.as_slice(),
/// ))
/// }
///
/// fn load_image_from_memory(image_data: &[u8]) -> Result<ColorImage, image::ImageError> {
/// use image::GenericImageView as _;
/// let image = image::load_from_memory(image_data)?;
/// let size = [image.width() as _, image.height() as _];
/// let image_buffer = image.to_rgba8();
/// let pixels = image_buffer.as_flat_samples();
/// Ok(ColorImage::from_rgba_unmultiplied(
/// size,
/// pixels.as_slice(),
/// ))
/// }
/// ```
pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
assert_eq!(size[0] * size[1] * 4, rgba.len());
let pixels = rgba
.chunks_exact(4)
.map(|p| Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
.collect();
Self { size, pixels }
}
/// An example color image, useful for tests.
pub fn example() -> Self {
let width = 128;
let height = 64;
let mut img = Self::new([width, height], Color32::TRANSPARENT);
for y in 0..height {
for x in 0..width {
let h = x as f32 / width as f32;
let s = 1.0;
let v = 1.0;
let a = y as f32 / height as f32;
img[(x, y)] = crate::color::Hsva { h, s, v, a }.into();
}
}
img
}
#[inline]
pub fn width(&self) -> usize {
self.size[0]
}
#[inline]
pub fn height(&self) -> usize {
self.size[1]
}
}
impl std::ops::Index<(usize, usize)> for ColorImage {
type Output = Color32;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &Color32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&self.pixels[y * w + x]
}
}
impl std::ops::IndexMut<(usize, usize)> for ColorImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut Color32 {
let [w, h] = self.size;
assert!(x < w && y < h);
&mut self.pixels[y * w + x]
}
}
impl From<ColorImage> for ImageData {
#[inline(always)]
fn from(image: ColorImage) -> Self {
Self::Color(image)
}
}
// ----------------------------------------------------------------------------
/// An 8-bit image, representing difference levels of transparent white.
///
/// Used for the font texture
#[derive(Clone, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub struct AlphaImage {
/// width, height
pub size: [usize; 2],
/// The alpha (linear space 0-255) of something white.
///
/// One byte per pixel. Often you want to use [`Self::srgba_pixels`] instead.
pub pixels: Vec<u8>,
}
impl AlphaImage {
pub fn new(size: [usize; 2]) -> Self {
Self {
size,
pixels: vec![0; size[0] * size[1]],
}
}
#[inline]
pub fn width(&self) -> usize {
self.size[0]
}
#[inline]
pub fn height(&self) -> usize {
self.size[1]
}
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
///
/// `gamma` should normally be set to 1.0.
/// If you are having problems with text looking skinny and pixelated, try
/// setting a lower gamma, e.g. `0.5`.
pub fn srgba_pixels(
&'_ self,
gamma: f32,
) -> impl ExactSizeIterator<Item = super::Color32> + '_ {
let srgba_from_alpha_lut: Vec<Color32> = (0..=255)
.map(|a| {
let a = super::color::linear_f32_from_linear_u8(a).powf(gamma);
super::Rgba::from_white_alpha(a).into()
})
.collect();
self.pixels
.iter()
.map(move |&a| srgba_from_alpha_lut[a as usize])
}
}
impl std::ops::Index<(usize, usize)> for AlphaImage {
type Output = u8;
#[inline]
fn index(&self, (x, y): (usize, usize)) -> &u8 {
let [w, h] = self.size;
assert!(x < w && y < h);
&self.pixels[y * w + x]
}
}
impl std::ops::IndexMut<(usize, usize)> for AlphaImage {
#[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 {
let [w, h] = self.size;
assert!(x < w && y < h);
&mut self.pixels[y * w + x]
}
}
impl From<AlphaImage> for ImageData {
#[inline(always)]
fn from(image: AlphaImage) -> Self {
Self::Alpha(image)
}
}

View File

@ -88,6 +88,7 @@
#![allow(clippy::manual_range_contains)] #![allow(clippy::manual_range_contains)]
pub mod color; pub mod color;
pub mod image;
mod mesh; mod mesh;
pub mod mutex; pub mod mutex;
mod shadow; mod shadow;
@ -98,10 +99,13 @@ mod stroke;
pub mod tessellator; pub mod tessellator;
pub mod text; pub mod text;
mod texture_atlas; mod texture_atlas;
mod texture_handle;
pub mod textures;
pub mod util; pub mod util;
pub use { pub use {
color::{Color32, Rgba}, color::{Color32, Rgba},
image::{AlphaImage, ColorImage, ImageData},
mesh::{Mesh, Mesh16, Vertex}, mesh::{Mesh, Mesh16, Vertex},
shadow::Shadow, shadow::Shadow,
shape::{CircleShape, PathShape, RectShape, Shape, TextShape}, shape::{CircleShape, PathShape, RectShape, Shape, TextShape},
@ -110,6 +114,8 @@ pub use {
tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, tessellator::{tessellate_shapes, TessellationOptions, Tessellator},
text::{Fonts, Galley, TextStyle}, text::{Fonts, Galley, TextStyle},
texture_atlas::{FontImage, TextureAtlas}, texture_atlas::{FontImage, TextureAtlas},
texture_handle::TextureHandle,
textures::TextureManager,
}; };
pub use emath::{pos2, vec2, Pos2, Rect, Vec2}; pub use emath::{pos2, vec2, Pos2, Rect, Vec2};
@ -124,21 +130,25 @@ pub use emath;
pub const WHITE_UV: emath::Pos2 = emath::pos2(0.0, 0.0); pub const WHITE_UV: emath::Pos2 = emath::pos2(0.0, 0.0);
/// What texture to use in a [`Mesh`] mesh. /// What texture to use in a [`Mesh`] mesh.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] ///
/// If you don't want to use a texture, use `TextureId::Epaint(0)` and the [`WHITE_UV`] for uv-coord.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
pub enum TextureId { pub enum TextureId {
/// The egui font texture. /// Textures allocated using [`TextureManager`].
/// If you don't want to use a texture, pick this and the [`WHITE_UV`] for uv-coord. ///
Egui, /// The first texture (`TextureId::Epaint(0)`) is used for the font data.
Managed(u64),
/// Your own texture, defined in any which way you want. /// 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. /// The backend renderer will presumably use this to look up what texture to use.
User(u64), User(u64),
} }
impl Default for TextureId { impl Default for TextureId {
/// The epaint font texture.
fn default() -> Self { fn default() -> Self {
Self::Egui Self::Managed(0)
} }
} }

View File

@ -105,7 +105,7 @@ impl Mesh {
#[inline(always)] #[inline(always)]
pub fn colored_vertex(&mut self, pos: Pos2, color: Color32) { pub fn colored_vertex(&mut self, pos: Pos2, color: Color32) {
crate::epaint_assert!(self.texture_id == TextureId::Egui); crate::epaint_assert!(self.texture_id == TextureId::default());
self.vertices.push(Vertex { self.vertices.push(Vertex {
pos, pos,
uv: WHITE_UV, uv: WHITE_UV,
@ -168,7 +168,7 @@ impl Mesh {
/// Uniformly colored rectangle. /// Uniformly colored rectangle.
#[inline(always)] #[inline(always)]
pub fn add_colored_rect(&mut self, rect: Rect, color: Color32) { pub fn add_colored_rect(&mut self, rect: Rect, color: Color32) {
crate::epaint_assert!(self.texture_id == TextureId::Egui); crate::epaint_assert!(self.texture_id == TextureId::default());
self.add_rect_with_uv(rect, [WHITE_UV, WHITE_UV].into(), color); self.add_rect_with_uv(rect, [WHITE_UV, WHITE_UV].into(), color);
} }

View File

@ -149,7 +149,7 @@ impl Shape {
if let Shape::Mesh(mesh) = self { if let Shape::Mesh(mesh) = self {
mesh.texture_id mesh.texture_id
} else { } else {
super::TextureId::Egui super::TextureId::default()
} }
} }

View File

@ -376,12 +376,12 @@ fn allocate_glyph(
} else { } else {
let glyph_pos = atlas.allocate((glyph_width, glyph_height)); let glyph_pos = atlas.allocate((glyph_width, glyph_height));
let texture = atlas.image_mut(); let image = atlas.image_mut();
glyph.draw(|x, y, v| { glyph.draw(|x, y, v| {
if v > 0.0 { if v > 0.0 {
let px = glyph_pos.0 + x as usize; let px = glyph_pos.0 + x as usize;
let py = glyph_pos.1 + y as usize; let py = glyph_pos.1 + y as usize;
texture[(px, py)] = (v * 255.0).round() as u8; image.image[(px, py)] = (v * 255.0).round() as u8;
} }
}); });

View File

@ -28,7 +28,7 @@ pub enum TextStyle {
} }
impl TextStyle { impl TextStyle {
pub fn all() -> impl Iterator<Item = TextStyle> { pub fn all() -> impl ExactSizeIterator<Item = TextStyle> {
[ [
TextStyle::Small, TextStyle::Small,
TextStyle::Body, TextStyle::Body,
@ -253,13 +253,13 @@ impl Fonts {
// We want an atlas big enough to be able to include all the Emojis in the `TextStyle::Heading`, // We want an atlas big enough to be able to include all the Emojis in the `TextStyle::Heading`,
// so we can show the Emoji picker demo window. // so we can show the Emoji picker demo window.
let mut atlas = TextureAtlas::new(2048, 64); let mut atlas = TextureAtlas::new([2048, 64]);
{ {
// Make the top left pixel fully white: // Make the top left pixel fully white:
let pos = atlas.allocate((1, 1)); let pos = atlas.allocate((1, 1));
assert_eq!(pos, (0, 0)); assert_eq!(pos, (0, 0));
atlas.image_mut()[pos] = 255; atlas.image_mut().image[pos] = 255;
} }
let atlas = Arc::new(Mutex::new(atlas)); let atlas = Arc::new(Mutex::new(atlas));
@ -287,7 +287,7 @@ impl Fonts {
let mut atlas = atlas.lock(); let mut atlas = atlas.lock();
let texture = atlas.image_mut(); let texture = atlas.image_mut();
// Make sure we seed the texture version with something unique based on the default characters: // Make sure we seed the texture version with something unique based on the default characters:
texture.version = crate::util::hash(&texture.pixels); texture.version = crate::util::hash(&texture.image);
} }
Self { Self {
@ -295,7 +295,7 @@ impl Fonts {
definitions, definitions,
fonts, fonts,
atlas, atlas,
buffered_font_image: Default::default(), //atlas.lock().texture().clone(); buffered_font_image: Default::default(),
galley_cache: Default::default(), galley_cache: Default::default(),
} }
} }

View File

@ -1,59 +1,30 @@
use crate::image::AlphaImage;
/// An 8-bit texture containing font data. /// An 8-bit texture containing font data.
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct FontImage { pub struct FontImage {
/// e.g. a hash of the data. Use this to detect changes! /// e.g. a hash of the data. Use this to detect changes!
/// If the texture changes, this too will change. /// If the texture changes, this too will change.
pub version: u64, pub version: u64,
pub width: usize,
pub height: usize, /// The actual image data.
/// The alpha (linear space 0-255) of something white. pub image: AlphaImage,
///
/// One byte per pixel. Often you want to use [`Self::srgba_pixels`] instead.
pub pixels: Vec<u8>,
} }
impl FontImage { impl FontImage {
#[inline]
pub fn size(&self) -> [usize; 2] { pub fn size(&self) -> [usize; 2] {
[self.width, self.height] self.image.size
} }
/// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom.
///
/// `gamma` should normally be set to 1.0.
/// If you are having problems with egui text looking skinny and pixelated, try
/// setting a lower gamma, e.g. `0.5`.
pub fn srgba_pixels(&'_ self, gamma: f32) -> impl Iterator<Item = super::Color32> + '_ {
use super::Color32;
let srgba_from_luminance_lut: Vec<Color32> = (0..=255)
.map(|a| {
let a = super::color::linear_f32_from_linear_u8(a).powf(gamma);
super::Rgba::from_white_alpha(a).into()
})
.collect();
self.pixels
.iter()
.map(move |&l| srgba_from_luminance_lut[l as usize])
}
}
impl std::ops::Index<(usize, usize)> for FontImage {
type Output = u8;
#[inline] #[inline]
fn index(&self, (x, y): (usize, usize)) -> &u8 { pub fn width(&self) -> usize {
assert!(x < self.width); self.image.size[0]
assert!(y < self.height);
&self.pixels[y * self.width + x]
} }
}
impl std::ops::IndexMut<(usize, usize)> for FontImage {
#[inline] #[inline]
fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 { pub fn height(&self) -> usize {
assert!(x < self.width); self.image.size[1]
assert!(y < self.height);
&mut self.pixels[y * self.width + x]
} }
} }
@ -70,13 +41,11 @@ pub struct TextureAtlas {
} }
impl TextureAtlas { impl TextureAtlas {
pub fn new(width: usize, height: usize) -> Self { pub fn new(size: [usize; 2]) -> Self {
Self { Self {
image: FontImage { image: FontImage {
version: 0, version: 0,
width, image: AlphaImage::new(size),
height,
pixels: vec![0; width * height],
}, },
..Default::default() ..Default::default()
} }
@ -99,12 +68,12 @@ impl TextureAtlas {
const PADDING: usize = 1; const PADDING: usize = 1;
assert!( assert!(
w <= self.image.width, w <= self.image.width(),
"Tried to allocate a {} wide glyph in a {} wide texture atlas", "Tried to allocate a {} wide glyph in a {} wide texture atlas",
w, w,
self.image.width self.image.width()
); );
if self.cursor.0 + w > self.image.width { if self.cursor.0 + w > self.image.width() {
// New row: // New row:
self.cursor.0 = 0; self.cursor.0 = 0;
self.cursor.1 += self.row_height + PADDING; self.cursor.1 += self.row_height + PADDING;
@ -112,15 +81,7 @@ impl TextureAtlas {
} }
self.row_height = self.row_height.max(h); self.row_height = self.row_height.max(h);
while self.cursor.1 + self.row_height >= self.image.height { resize_to_min_height(&mut self.image.image, self.cursor.1 + self.row_height);
self.image.height *= 2;
}
if self.image.width * self.image.height > self.image.pixels.len() {
self.image
.pixels
.resize(self.image.width * self.image.height, 0);
}
let pos = self.cursor; let pos = self.cursor;
self.cursor.0 += w + PADDING; self.cursor.0 += w + PADDING;
@ -128,3 +89,13 @@ impl TextureAtlas {
(pos.0 as usize, pos.1 as usize) (pos.0 as usize, pos.1 as usize)
} }
} }
fn resize_to_min_height(image: &mut AlphaImage, min_height: usize) {
while min_height >= image.height() {
image.size[1] *= 2; // double the height
}
if image.width() * image.height() > image.pixels.len() {
image.pixels.resize(image.width() * image.height(), 0);
}
}

View File

@ -0,0 +1,107 @@
use crate::{
emath::NumExt,
mutex::{Arc, RwLock},
ImageData, TextureId, TextureManager,
};
/// Used to paint images.
///
/// An _image_ is pixels stored in RAM, and represented using [`ImageData`].
/// Before you can paint it however, you need to convert it to a _texture_.
///
/// If you are using egui, use `egui::Context::load_texture`.
///
/// The [`TextureHandle`] can be cloned cheaply.
/// When the last [`TextureHandle`] for specific texture is dropped, the texture is freed.
///
/// See also [`TextureManager`].
#[must_use]
pub struct TextureHandle {
tex_mngr: Arc<RwLock<TextureManager>>,
id: TextureId,
}
impl Drop for TextureHandle {
fn drop(&mut self) {
self.tex_mngr.write().free(self.id);
}
}
impl Clone for TextureHandle {
fn clone(&self) -> Self {
self.tex_mngr.write().retain(self.id);
Self {
tex_mngr: self.tex_mngr.clone(),
id: self.id,
}
}
}
impl PartialEq for TextureHandle {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
impl Eq for TextureHandle {}
impl std::hash::Hash for TextureHandle {
#[inline]
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.id.hash(state);
}
}
impl TextureHandle {
/// If you are using egui, use `egui::Context::load_texture` instead.
pub fn new(tex_mngr: Arc<RwLock<TextureManager>>, id: TextureId) -> Self {
Self { tex_mngr, id }
}
#[inline]
pub fn id(&self) -> TextureId {
self.id
}
/// Assign a new image to an existing texture.
pub fn set(&mut self, image: impl Into<ImageData>) {
self.tex_mngr.write().set(self.id, image.into());
}
/// width x height
pub fn size(&self) -> [usize; 2] {
self.tex_mngr.read().meta(self.id).unwrap().size
}
/// width x height
pub fn size_vec2(&self) -> crate::Vec2 {
let [w, h] = self.size();
crate::Vec2::new(w as f32, h as f32)
}
/// width / height
pub fn aspect_ratio(&self) -> f32 {
let [w, h] = self.size();
w as f32 / h.at_least(1) as f32
}
/// Debug-name.
pub fn name(&self) -> String {
self.tex_mngr.read().meta(self.id).unwrap().name.clone()
}
}
impl From<&TextureHandle> for TextureId {
#[inline(always)]
fn from(handle: &TextureHandle) -> Self {
handle.id()
}
}
impl From<&mut TextureHandle> for TextureId {
#[inline(always)]
fn from(handle: &mut TextureHandle) -> Self {
handle.id()
}
}

161
epaint/src/textures.rs Normal file
View File

@ -0,0 +1,161 @@
use crate::{image::ImageData, TextureId};
use ahash::AHashMap;
// ----------------------------------------------------------------------------
/// Low-level manager for allocating textures.
///
/// Communicates with the painting subsystem using [`Self::take_delta`].
#[derive(Default)]
pub struct TextureManager {
/// We allocate texture id:s linearly.
next_id: u64,
/// Information about currently allocated textures.
metas: AHashMap<TextureId, TextureMeta>,
delta: TexturesDelta,
}
impl TextureManager {
/// Allocate a new texture.
///
/// The given name can be useful for later debugging.
///
/// The returned [`TextureId`] will be [`TextureId::Managed`], with an index
/// starting from zero and increasing with each call to [`Self::alloc`].
///
/// The first texture you allocate will be `TextureId::Managed(0) == TexureId::default()` and
/// MUST have a white pixel at (0,0) ([`crate::WHITE_UV`]).
///
/// The texture is given a retain-count of `1`, requiring one call to [`Self::free`] to free it.
pub fn alloc(&mut self, name: String, image: ImageData) -> TextureId {
let id = TextureId::Managed(self.next_id);
self.next_id += 1;
self.metas.entry(id).or_insert_with(|| TextureMeta {
name,
size: image.size(),
bytes_per_pixel: image.bytes_per_pixel(),
retain_count: 1,
});
self.delta.set.insert(id, image);
id
}
/// Assign a new image to an existing texture.
pub fn set(&mut self, id: TextureId, image: ImageData) {
if let Some(meta) = self.metas.get_mut(&id) {
meta.size = image.size();
meta.bytes_per_pixel = image.bytes_per_pixel();
self.delta.set.insert(id, image);
} else {
crate::epaint_assert!(
false,
"Tried setting texture {:?} which is not allocated",
id
);
}
}
/// Free an existing texture.
pub fn free(&mut self, id: TextureId) {
if let std::collections::hash_map::Entry::Occupied(mut entry) = self.metas.entry(id) {
let meta = entry.get_mut();
meta.retain_count -= 1;
if meta.retain_count == 0 {
entry.remove();
self.delta.free.push(id);
}
} else {
crate::epaint_assert!(
false,
"Tried freeing texture {:?} which is not allocated",
id
);
}
}
/// Increase the retain-count of the given texture.
///
/// For each time you call [`Self::retain`] you must call [`Self::free`] on additional time.
pub fn retain(&mut self, id: TextureId) {
if let Some(meta) = self.metas.get_mut(&id) {
meta.retain_count += 1;
} else {
crate::epaint_assert!(
false,
"Tried retaining texture {:?} which is not allocated",
id
);
}
}
/// Take and reset changes since last frame.
///
/// These should be applied to the painting subsystem each frame.
pub fn take_delta(&mut self) -> TexturesDelta {
std::mem::take(&mut self.delta)
}
/// Get meta-data about a specific texture.
pub fn meta(&self, id: TextureId) -> Option<&TextureMeta> {
self.metas.get(&id)
}
/// Get meta-data about all allocated textures in some arbitrary order.
pub fn allocated(&self) -> impl ExactSizeIterator<Item = (&TextureId, &TextureMeta)> {
self.metas.iter()
}
/// Total number of allocated textures.
pub fn num_allocated(&self) -> usize {
self.metas.len()
}
}
/// Meta-data about an allocated texture.
#[derive(Clone, Debug, PartialEq)]
pub struct TextureMeta {
/// A human-readable name useful for debugging.
pub name: String,
/// width x height
pub size: [usize; 2],
/// 4 or 1
pub bytes_per_pixel: usize,
/// Free when this reaches zero.
pub retain_count: usize,
}
impl TextureMeta {
/// Size in bytes.
/// width x height x [`Self::bytes_per_pixel`].
pub fn bytes_used(&self) -> usize {
self.size[0] * self.size[1] * self.bytes_per_pixel
}
}
// ----------------------------------------------------------------------------
/// What has been allocated and freed during the last period.
///
/// These are commands given to the integration painter.
#[derive(Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))]
#[must_use = "The painter must take care of this"]
pub struct TexturesDelta {
/// New or changed textures. Apply before painting.
pub set: AHashMap<TextureId, ImageData>,
/// Texture to free after painting.
pub free: Vec<TextureId>,
}
impl TexturesDelta {
pub fn append(&mut self, mut newer: TexturesDelta) {
self.set.extend(newer.set.into_iter());
self.free.append(&mut newer.free);
}
}

View File

@ -328,31 +328,7 @@ impl Frame {
/// for integrations only: call once per frame /// for integrations only: call once per frame
pub fn take_app_output(&self) -> crate::backend::AppOutput { pub fn take_app_output(&self) -> crate::backend::AppOutput {
let mut lock = self.lock(); std::mem::take(&mut self.lock().output)
let next_id = lock.output.tex_allocation_data.next_id;
let app_output = std::mem::take(&mut lock.output);
lock.output.tex_allocation_data.next_id = next_id;
app_output
}
/// Allocate a texture. Free it again with [`Self::free_texture`].
pub fn alloc_texture(&self, image: Image) -> egui::TextureId {
self.lock().output.tex_allocation_data.alloc(image)
}
/// Free a texture that has been previously allocated with [`Self::alloc_texture`]. Idempotent.
pub fn free_texture(&self, id: egui::TextureId) {
self.lock().output.tex_allocation_data.free(id);
}
}
impl TextureAllocator for Frame {
fn alloc(&self, image: Image) -> egui::TextureId {
self.lock().output.tex_allocation_data.alloc(image)
}
fn free(&self, id: egui::TextureId) {
self.lock().output.tex_allocation_data.free(id);
} }
} }
@ -386,41 +362,6 @@ pub struct IntegrationInfo {
pub native_pixels_per_point: Option<f32>, pub native_pixels_per_point: Option<f32>,
} }
/// How to allocate textures (images) to use in [`egui`].
pub trait TextureAllocator {
/// Allocate a new user texture.
///
/// There is no way to change a texture.
/// Instead allocate a new texture and free the previous one with [`Self::free`].
fn alloc(&self, image: Image) -> egui::TextureId;
/// Free the given texture.
fn free(&self, id: egui::TextureId);
}
/// A 2D color image in RAM.
#[derive(Clone, Default)]
pub struct Image {
/// width, height
pub size: [usize; 2],
/// The pixels, row by row, from top to bottom.
pub pixels: Vec<egui::Color32>,
}
impl Image {
/// Create an `Image` from flat RGBA data.
/// Panics unless `size[0] * size[1] * 4 == rgba.len()`.
/// This is usually what you want to use after having loaded an image.
pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
assert_eq!(size[0] * size[1] * 4, rgba.len());
let pixels = rgba
.chunks_exact(4)
.map(|p| egui::Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
.collect();
Self { size, pixels }
}
}
/// Abstraction for platform dependent texture reference /// Abstraction for platform dependent texture reference
pub trait NativeTexture { pub trait NativeTexture {
/// The native texture type. /// The native texture type.
@ -482,8 +423,6 @@ pub const APP_KEY: &str = "app";
/// You only need to look here if you are writing a backend for `epi`. /// You only need to look here if you are writing a backend for `epi`.
pub mod backend { pub mod backend {
use std::collections::HashMap;
use super::*; use super::*;
/// How to signal the [`egui`] integration that a repaint is required. /// How to signal the [`egui`] integration that a repaint is required.
@ -498,49 +437,14 @@ pub mod backend {
pub struct FrameData { pub struct FrameData {
/// Information about the integration. /// Information about the integration.
pub info: IntegrationInfo, pub info: IntegrationInfo,
/// Where the app can issue commands back to the integration. /// Where the app can issue commands back to the integration.
pub output: AppOutput, pub output: AppOutput,
/// If you need to request a repaint from another thread, clone this and send it to that other thread. /// If you need to request a repaint from another thread, clone this and send it to that other thread.
pub repaint_signal: std::sync::Arc<dyn RepaintSignal>, pub repaint_signal: std::sync::Arc<dyn RepaintSignal>,
} }
/// The data needed in order to allocate and free textures/images.
#[derive(Default)]
#[must_use]
pub struct TexAllocationData {
/// We allocate texture id linearly.
pub(crate) next_id: u64,
/// New creations this frame
pub creations: HashMap<u64, Image>,
/// destructions this frame.
pub destructions: Vec<u64>,
}
impl TexAllocationData {
/// Should only be used by integrations
pub fn take(&mut self) -> Self {
let next_id = self.next_id;
let ret = std::mem::take(self);
self.next_id = next_id;
ret
}
/// Allocate a new texture.
pub fn alloc(&mut self, image: Image) -> egui::TextureId {
let id = self.next_id;
self.next_id += 1;
self.creations.insert(id, image);
egui::TextureId::User(id)
}
/// Free an existing texture.
pub fn free(&mut self, id: egui::TextureId) {
if let egui::TextureId::User(id) = id {
self.destructions.push(id);
}
}
}
/// Action that can be taken by the user app. /// Action that can be taken by the user app.
#[derive(Default)] #[derive(Default)]
#[must_use] #[must_use]
@ -560,8 +464,5 @@ pub mod backend {
/// Set to true to drag window while primary mouse button is down. /// Set to true to drag window while primary mouse button is down.
pub drag_window: bool, pub drag_window: bool,
/// A way to allocate textures (on integrations that support it).
pub tex_allocation_data: TexAllocationData,
} }
} }