From 66d80e25195a233a427fc4c5e5c1c3177863df44 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Sat, 15 Jan 2022 13:59:52 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 2 + Cargo.lock | 4 + eframe/CHANGELOG.md | 1 + eframe/examples/image.rs | 45 ++-- egui-winit/src/epi.rs | 28 +- egui-winit/src/lib.rs | 33 ++- egui/src/context.rs | 153 ++++++++++- egui/src/data/input.rs | 7 +- egui/src/data/output.rs | 5 + egui/src/input_state.rs | 7 +- egui/src/introspection.rs | 10 +- egui/src/lib.rs | 10 +- egui/src/response.rs | 2 +- egui/src/ui.rs | 25 +- egui/src/util/history.rs | 4 +- egui/src/widgets/button.rs | 2 +- egui/src/widgets/image.rs | 34 ++- egui/src/widgets/label.rs | 6 +- egui/src/widgets/plot/items/mod.rs | 4 +- egui/src/widgets/plot/items/values.rs | 2 +- egui_demo_lib/src/apps/color_test.rs | 138 ++++------ egui_demo_lib/src/apps/demo/plot_demo.rs | 17 +- egui_demo_lib/src/apps/demo/widget_gallery.rs | 20 +- egui_demo_lib/src/apps/http_app.rs | 50 ++-- egui_demo_lib/src/syntax_highlighting.rs | 2 +- egui_glium/CHANGELOG.md | 2 + egui_glium/Cargo.toml | 7 +- egui_glium/examples/native_texture.rs | 4 +- egui_glium/examples/pure_glium.rs | 4 +- egui_glium/src/epi_backend.rs | 7 +- egui_glium/src/lib.rs | 47 ++-- egui_glium/src/painter.rs | 129 ++++------ egui_glow/CHANGELOG.md | 2 + egui_glow/Cargo.toml | 7 +- egui_glow/examples/pure_glow.rs | 4 +- egui_glow/src/epi_backend.rs | 9 +- egui_glow/src/lib.rs | 34 ++- egui_glow/src/misc_util.rs | 4 - egui_glow/src/painter.rs | 151 +++++------ egui_glow/src/post_process.rs | 2 +- egui_web/Cargo.toml | 3 + egui_web/src/backend.rs | 49 ++-- egui_web/src/glow_wrapping.rs | 12 +- egui_web/src/lib.rs | 6 +- egui_web/src/painter.rs | 6 +- egui_web/src/webgl1.rs | 159 ++++-------- egui_web/src/webgl2.rs | 154 ++++------- emath/src/align.rs | 2 +- epaint/CHANGELOG.md | 3 +- epaint/src/image.rs | 241 ++++++++++++++++++ epaint/src/lib.rs | 22 +- epaint/src/mesh.rs | 4 +- epaint/src/shape.rs | 2 +- epaint/src/text/font.rs | 4 +- epaint/src/text/fonts.rs | 10 +- epaint/src/texture_atlas.rs | 83 ++---- epaint/src/texture_handle.rs | 107 ++++++++ epaint/src/textures.rs | 161 ++++++++++++ epi/src/lib.rs | 105 +------- 59 files changed, 1297 insertions(+), 860 deletions(-) create mode 100644 epaint/src/image.rs create mode 100644 epaint/src/texture_handle.rs create mode 100644 epaint/src/textures.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 9120a867..7f60faf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w ## Unreleased ### 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`. ### 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)). * Renamed `Ui::visible` to `Ui::is_visible`. * 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 🐛 * Context menu now respects the theme ([#1043](https://github.com/emilk/egui/pull/1043)) diff --git a/Cargo.lock b/Cargo.lock index c3767604..f8973c2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -841,6 +841,8 @@ dependencies = [ name = "egui_glium" version = "0.16.0" dependencies = [ + "ahash", + "bytemuck", "egui", "egui-winit", "epi", @@ -852,6 +854,7 @@ dependencies = [ name = "egui_glow" version = "0.16.0" dependencies = [ + "bytemuck", "egui", "egui-winit", "epi", @@ -867,6 +870,7 @@ dependencies = [ name = "egui_web" version = "0.16.0" dependencies = [ + "bytemuck", "egui", "egui_glow", "epi", diff --git a/eframe/CHANGELOG.md b/eframe/CHANGELOG.md index a6480965..f74e0dad 100644 --- a/eframe/CHANGELOG.md +++ b/eframe/CHANGELOG.md @@ -5,6 +5,7 @@ NOTE: [`egui_web`](egui_web/CHANGELOG.md), [`egui-winit`](egui-winit/CHANGELOG.m ## 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 web painter is now `egui_glow` (instead of WebGL) ([#1020](https://github.com/emilk/egui/pull/1020)). diff --git a/eframe/examples/image.rs b/eframe/examples/image.rs index 9a26ffce..5a151770 100644 --- a/eframe/examples/image.rs +++ b/eframe/examples/image.rs @@ -4,7 +4,7 @@ use eframe::{egui, epi}; #[derive(Default)] struct MyApp { - texture: Option<(egui::Vec2, egui::TextureId)>, + texture: Option, } impl epi::App for MyApp { @@ -12,31 +12,18 @@ impl epi::App for MyApp { "Show an image with eframe/egui" } - fn update(&mut self, ctx: &egui::Context, frame: &epi::Frame) { - if self.texture.is_none() { - // Load the image: - let image_data = include_bytes!("rust-logo-256x256.png"); - 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)); - } + fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { + let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| { + let image = load_image(include_bytes!("rust-logo-256x256.png")).unwrap(); + ctx.load_texture("rust-logo", image) + }); egui::CentralPanel::default().show(ctx, |ui| { - if let Some((size, texture)) = self.texture { - ui.heading("This is an image:"); - ui.image(texture, size); + ui.heading("This is an image:"); + ui.image(texture, texture.size_vec2()); - ui.heading("This is an image you can click:"); - ui.add(egui::ImageButton::new(texture, size)); - } + ui.heading("This is an image you can click:"); + ui.add(egui::ImageButton::new(texture, texture.size_vec2())); }); } } @@ -45,3 +32,15 @@ fn main() { let options = eframe::NativeOptions::default(); eframe::run_native(Box::new(MyApp::default()), options); } + +fn load_image(image_data: &[u8]) -> Result { + 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(), + )) +} diff --git a/egui-winit/src/epi.rs b/egui-winit/src/epi.rs index ea4353bf..64ce7a9d 100644 --- a/egui-winit/src/epi.rs +++ b/egui-winit/src/epi.rs @@ -55,10 +55,9 @@ pub fn handle_app_output( window: &winit::window::Window, current_pixels_per_point: f32, app_output: epi::backend::AppOutput, -) -> epi::backend::TexAllocationData { +) { let epi::backend::AppOutput { quit: _, - tex_allocation_data, window_size, window_title, decorated, @@ -86,8 +85,6 @@ pub fn handle_app_output( if drag_window { let _ = window.drag_window(); } - - tex_allocation_data } // ---------------------------------------------------------------------------- @@ -244,16 +241,14 @@ impl EpiIntegration { .setup(&self.egui_ctx, &self.frame, self.persistence.storage()); let app_output = self.frame.take_app_output(); self.quit |= app_output.quit; - let tex_alloc_data = - 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 + crate::epi::handle_app_output(window, self.egui_ctx.pixels_per_point(), app_output); } fn warm_up(&mut self, window: &winit::window::Window) { let saved_memory: egui::Memory = self.egui_ctx.memory().clone(); self.egui_ctx.memory().set_everything_is_visible(true); - let (_, tex_alloc_data, _) = self.update(window); - self.frame.lock().output.tex_allocation_data = tex_alloc_data; // handle it next frame + let (_, textures_delta, _) = self.update(window); + 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.clear_animations(); } @@ -273,11 +268,7 @@ impl EpiIntegration { pub fn update( &mut self, window: &winit::window::Window, - ) -> ( - bool, - epi::backend::TexAllocationData, - Vec, - ) { + ) -> (bool, egui::TexturesDelta, Vec) { let frame_start = instant::Instant::now(); let raw_input = self.egui_winit.take_egui_input(window); @@ -286,18 +277,19 @@ impl EpiIntegration { }); let needs_repaint = egui_output.needs_repaint; - self.egui_winit + let textures_delta = self + .egui_winit .handle_output(window, &self.egui_ctx, egui_output); let app_output = self.frame.take_app_output(); 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; 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) { diff --git a/egui-winit/src/lib.rs b/egui-winit/src/lib.rs index 8bfdcc76..b87bbdd2 100644 --- a/egui-winit/src/lib.rs +++ b/egui-winit/src/lib.rs @@ -512,26 +512,39 @@ impl State { window: &winit::window::Window, egui_ctx: &egui::Context, output: egui::Output, - ) { - self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI - + ) -> egui::TexturesDelta { if egui_ctx.memory().options.screen_reader { 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 { - open_url(&open.url); + self.current_pixels_per_point = egui_ctx.pixels_per_point(); // someone can have changed it to scale the UI + + 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() { - self.clipboard.set(output.copied_text); + if !copied_text.is_empty() { + 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 }); } + + textures_delta } 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")] if let Err(err) = webbrowser::open(_url) { eprintln!("Failed to open url: {}", err); diff --git a/egui/src/context.rs b/egui/src/context.rs index ef9bd976..85daee62 100644 --- a/egui/src/context.rs +++ b/egui/src/context.rs @@ -2,18 +2,39 @@ use crate::{ animation_manager::AnimationManager, data::output::Output, frame_state::FrameState, - input_state::*, layers::GraphicLayers, *, + input_state::*, layers::GraphicLayers, TextureHandle, *, }; use epaint::{mutex::*, stats::*, text::Fonts, *}; // ---------------------------------------------------------------------------- +struct WrappedTextureManager(Arc>); + +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)] struct ContextImpl { /// `None` until the start of the first frame. fonts: Option, memory: Memory, animation_manager: AnimationManager, + latest_font_image_version: Option, + tex_manager: WrappedTextureManager, input: InputState, @@ -157,7 +178,7 @@ impl Context { /// /// You can alternatively run [`Self::begin_frame`] and [`Context::end_frame`]. /// - /// ``` rust + /// ``` /// // One egui context that you keep reusing: /// let mut ctx = egui::Context::default(); /// @@ -183,7 +204,7 @@ impl Context { /// An alternative to calling [`Self::run`]. /// - /// ``` rust + /// ``` /// // One egui context that you keep reusing: /// let mut ctx = egui::Context::default(); /// @@ -492,14 +513,6 @@ impl Context { 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 { - self.fonts().font_image() - } - /// Tell `egui` which fonts to use. /// /// 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, + /// } + /// + /// 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, + image: impl Into, + ) -> 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> { + self.read().tex_manager.0.clone() + } + // --------------------------------------------------------------------- /// Constrain the position of a window/area so it fits within the provided boundary. @@ -640,14 +701,30 @@ impl Context { self.request_repaint(); } + self.fonts().end_frame(); + { let ctx_impl = &mut *self.write(); ctx_impl .memory .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()); if self.read().repaint_requests > 0 { @@ -936,11 +1013,59 @@ impl Context { }); CollapsingHeader::new("📊 Paint stats") - .default_open(true) + .default_open(false) .show(ui, |ui| { let paint_stats = self.write().paint_stats; 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) { diff --git a/egui/src/data/input.rs b/egui/src/data/input.rs index 2a5511b9..6465ec2f 100644 --- a/egui/src/data/input.rs +++ b/egui/src/data/input.rs @@ -375,10 +375,13 @@ impl RawInput { } ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); 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!("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"); + }); } } diff --git a/egui/src/data/output.rs b/egui/src/data/output.rs index 9947df86..664808b5 100644 --- a/egui/src/data/output.rs +++ b/egui/src/data/output.rs @@ -35,6 +35,9 @@ pub struct Output { /// Screen-space position of text edit cursor (used for IME). pub text_cursor_pos: Option, + + /// Texture changes since last frame. + pub textures_delta: epaint::textures::TexturesDelta, } impl Output { @@ -71,6 +74,7 @@ impl Output { mut events, mutable_text_under_cursor, text_cursor_pos, + textures_delta, } = newer; self.cursor_icon = cursor_icon; @@ -84,6 +88,7 @@ impl Output { self.events.append(&mut events); self.mutable_text_under_cursor = mutable_text_under_cursor; 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) diff --git a/egui/src/input_state.rs b/egui/src/input_state.rs index e1e1bb55..25e266ae 100644 --- a/egui/src/input_state.rs +++ b/egui/src/input_state.rs @@ -730,8 +730,11 @@ impl InputState { ui.label(format!("predicted_dt: {:.1} ms", 1e3 * predicted_dt)); ui.label(format!("modifiers: {:#?}", modifiers)); ui.label(format!("keys_down: {:?}", keys_down)); - ui.label(format!("events: {:?}", events)) - .on_hover_text("key presses etc"); + ui.scope(|ui| { + ui.set_min_height(150.0); + ui.label(format!("events: {:#?}", events)) + .on_hover_text("key presses etc"); + }); } } diff --git a/egui/src/introspection.rs b/egui/src/introspection.rs index bc2358a9..9ec2a97c 100644 --- a/egui/src/introspection.rs +++ b/egui/src/introspection.rs @@ -7,14 +7,16 @@ impl Widget for &epaint::FontImage { ui.vertical(|ui| { // Show font texture in demo Ui + let [width, height] = self.size(); + ui.label(format!( "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; } - 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() { size *= ui.available_width() / size.x; } @@ -27,7 +29,7 @@ impl Widget for &epaint::FontImage { ); 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 .on_hover_cursor(CursorIcon::ZoomIn) diff --git a/egui/src/lib.rs b/egui/src/lib.rs index d77671ea..59528e7e 100644 --- a/egui/src/lib.rs +++ b/egui/src/lib.rs @@ -58,7 +58,7 @@ //! //! ### Quick start //! -//! ``` rust +//! ``` //! # egui::__run_test_ui(|ui| { //! # let mut my_string = String::new(); //! # let mut my_boolean = true; @@ -218,7 +218,7 @@ //! 2. Wrap your panel contents in a [`ScrollArea`], or use [`Window::vscroll`] and [`Window::hscroll`]. //! 3. Use a justified layout: //! -//! ``` rust +//! ``` //! # egui::__run_test_ui(|ui| { //! ui.with_layout(egui::Layout::top_down_justified(egui::Align::Center), |ui| { //! ui.button("I am becoming wider as needed"); @@ -228,7 +228,7 @@ //! //! 4. Fill in extra space with emptiness: //! -//! ``` rust +//! ``` //! # egui::__run_test_ui(|ui| { //! 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::{ color, mutex, 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 { diff --git a/egui/src/response.rs b/egui/src/response.rs index dd975c7d..2d20d172 100644 --- a/egui/src/response.rs +++ b/egui/src/response.rs @@ -480,7 +480,7 @@ impl Response { /// Response to secondary clicks (right-clicks) by showing the given menu. /// - /// ``` rust + /// ``` /// # egui::__run_test_ui(|ui| { /// let response = ui.label("Right-click me!"); /// response.context_menu(|ui| { diff --git a/egui/src/ui.rs b/egui/src/ui.rs index 3d66589f..8c53e3f2 100644 --- a/egui/src/ui.rs +++ b/egui/src/ui.rs @@ -1341,9 +1341,30 @@ impl Ui { /// 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, + /// } + /// + /// 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] - pub fn image(&mut self, texture_id: TextureId, size: impl Into) -> Response { + pub fn image(&mut self, texture_id: impl Into, size: impl Into) -> Response { Image::new(texture_id, size).ui(self) } } diff --git a/egui/src/util/history.rs b/egui/src/util/history.rs index 7c02ed8c..bea3f97f 100644 --- a/egui/src/util/history.rs +++ b/egui/src/util/history.rs @@ -109,11 +109,11 @@ where /// `(time, value)` pairs /// Time difference between values can be zero, but never negative. // TODO: impl IntoIter - pub fn iter(&'_ self) -> impl Iterator + '_ { + pub fn iter(&'_ self) -> impl ExactSizeIterator + '_ { self.values.iter().map(|(time, value)| (*time, *value)) } - pub fn values(&'_ self) -> impl Iterator + '_ { + pub fn values(&'_ self) -> impl ExactSizeIterator + '_ { self.values.iter().map(|(_time, value)| *value) } diff --git a/egui/src/widgets/button.rs b/egui/src/widgets/button.rs index 599d77c1..919957ec 100644 --- a/egui/src/widgets/button.rs +++ b/egui/src/widgets/button.rs @@ -394,7 +394,7 @@ pub struct ImageButton { } impl ImageButton { - pub fn new(texture_id: TextureId, size: impl Into) -> Self { + pub fn new(texture_id: impl Into, size: impl Into) -> Self { Self { image: widgets::Image::new(texture_id, size), sense: Sense::click(), diff --git a/egui/src/widgets/image.rs b/egui/src/widgets/image.rs index ef1b88b6..61ea0d9a 100644 --- a/egui/src/widgets/image.rs +++ b/egui/src/widgets/image.rs @@ -2,17 +2,31 @@ use crate::*; /// An widget to show an image of a given size. /// -/// ``` -/// # egui::__run_test_ui(|ui| { -/// # let my_texture_id = egui::TextureId::User(0); -/// ui.add(egui::Image::new(my_texture_id, [640.0, 480.0])); +/// In order to display an image you must first acquire a [`TextureHandle`] +/// using [`Context::load_texture`]. /// -/// // Shorter version: -/// ui.image(my_texture_id, [640.0, 480.0]); -/// # }); +/// ``` +/// struct MyImage { +/// texture: Option, +/// } +/// +/// 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: +/// 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);`"] #[derive(Clone, Copy, Debug)] pub struct Image { @@ -25,9 +39,9 @@ pub struct Image { } impl Image { - pub fn new(texture_id: TextureId, size: impl Into) -> Self { + pub fn new(texture_id: impl Into, size: impl Into) -> Self { Self { - texture_id, + texture_id: texture_id.into(), uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), size: size.into(), bg_fill: Default::default(), diff --git a/egui/src/widgets/label.rs b/egui/src/widgets/label.rs index 915704f4..b819e08b 100644 --- a/egui/src/widgets/label.rs +++ b/egui/src/widgets/label.rs @@ -49,7 +49,7 @@ impl Label { /// 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. /// - /// ``` rust + /// ``` /// # use egui::{Label, Sense}; /// # egui::__run_test_ui(|ui| { /// if ui.add(Label::new("click me").sense(Sense::click())).clicked() { @@ -82,7 +82,9 @@ impl Label { } 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 available_width = ui.available_width(); diff --git a/egui/src/widgets/plot/items/mod.rs b/egui/src/widgets/plot/items/mod.rs index c6f3d31e..99f7fb86 100644 --- a/egui/src/widgets/plot/items/mod.rs +++ b/egui/src/widgets/plot/items/mod.rs @@ -1087,12 +1087,12 @@ pub struct PlotImage { impl PlotImage { /// Create a new image with position and size in plot coordinates. - pub fn new(texture_id: TextureId, position: Value, size: impl Into) -> Self { + pub fn new(texture_id: impl Into, position: Value, size: impl Into) -> Self { Self { position, name: Default::default(), highlight: false, - texture_id, + texture_id: texture_id.into(), uv: Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), size: size.into(), bg_fill: Default::default(), diff --git a/egui/src/widgets/plot/items/values.rs b/egui/src/widgets/plot/items/values.rs index 735f3e03..ce24a03d 100644 --- a/egui/src/widgets/plot/items/values.rs +++ b/egui/src/widgets/plot/items/values.rs @@ -297,7 +297,7 @@ pub enum MarkerShape { impl MarkerShape { /// Get a vector containing all marker shapes. - pub fn all() -> impl Iterator { + pub fn all() -> impl ExactSizeIterator { [ Self::Circle, Self::Diamond, diff --git a/egui_demo_lib/src/apps/color_test.rs b/egui_demo_lib/src/apps/color_test.rs index 1263aebd..f306e11a 100644 --- a/egui_demo_lib/src/apps/color_test.rs +++ b/egui_demo_lib/src/apps/color_test.rs @@ -43,14 +43,14 @@ impl epi::App for ColorTest { ui.separator(); } ScrollArea::both().auto_shrink([false; 2]).show(ui, |ui| { - self.ui(ui, Some(frame)); + self.ui(ui); }); }); } } 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.vertical_centered(|ui| { @@ -70,13 +70,7 @@ impl ColorTest { ui.spacing_mut().item_spacing.y = 0.0; // No spacing between gradients let g = Gradient::one_color(Color32::from_rgb(255, 165, 0)); self.vertex_gradient(ui, "orange rgb(255, 165, 0) - vertex", WHITE, &g); - self.tex_gradient( - ui, - tex_allocator, - "orange rgb(255, 165, 0) - texture", - WHITE, - &g, - ); + self.tex_gradient(ui, "orange rgb(255, 165, 0) - texture", WHITE, &g); }); ui.separator(); @@ -99,20 +93,18 @@ impl ColorTest { { let g = Gradient::one_color(Color32::from(tex_color * vertex_color)); self.vertex_gradient(ui, "Ground truth (vertices)", WHITE, &g); - self.tex_gradient(ui, tex_allocator, "Ground truth (texture)", WHITE, &g); - } - if let Some(tex_allocator) = tex_allocator { - ui.horizontal(|ui| { - let g = Gradient::one_color(Color32::from(tex_color)); - let tex = self.tex_mngr.get(tex_allocator, &g); - let texel_offset = 0.5 / (g.0.len() as f32); - 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).tint(vertex_color).uv(uv)) - .on_hover_text(format!("A texture that is {} texels wide", g.0.len())); - ui.label("GPU result"); - }); + self.tex_gradient(ui, "Ground truth (texture)", WHITE, &g); } + + ui.horizontal(|ui| { + let g = Gradient::one_color(Color32::from(tex_color)); + let tex = self.tex_mngr.get(ui.ctx(), &g); + let texel_offset = 0.5 / (g.0.len() as f32); + 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).tint(vertex_color).uv(uv)) + .on_hover_text(format!("A texture that is {} texels wide", g.0.len())); + ui.label("GPU result"); + }); }); ui.separator(); @@ -120,18 +112,18 @@ impl ColorTest { // TODO: test color multiplication (image tint), // 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 { ui.label("Notice the darkening in the center of the naive sRGB interpolation."); } ui.separator(); - self.show_gradients(ui, tex_allocator, RED, (TRANSPARENT, GREEN)); + self.show_gradients(ui, RED, (TRANSPARENT, GREEN)); ui.separator(); - self.show_gradients(ui, tex_allocator, WHITE, (TRANSPARENT, GREEN)); + self.show_gradients(ui, WHITE, (TRANSPARENT, GREEN)); if self.srgb { ui.label( "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: 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(); - self.show_gradients(ui, tex_allocator, BLACK, (TRANSPARENT, WHITE)); + self.show_gradients(ui, BLACK, (TRANSPARENT, WHITE)); ui.separator(); ui.label("Additive blending: add more and more blue to the red background:"); self.show_gradients( ui, - tex_allocator, RED, (TRANSPARENT, Color32::from_rgb_additive(0, 0, 255)), ); @@ -160,13 +151,7 @@ impl ColorTest { pixel_test(ui); } - fn show_gradients( - &mut self, - ui: &mut Ui, - tex_allocator: Option<&dyn epi::TextureAllocator>, - bg_fill: Color32, - (left, right): (Color32, Color32), - ) { + fn show_gradients(&mut self, ui: &mut Ui, bg_fill: Color32, (left, right): (Color32, Color32)) { let is_opaque = left.is_opaque() && right.is_opaque(); ui.horizontal(|ui| { @@ -186,13 +171,7 @@ impl ColorTest { if is_opaque { let g = Gradient::ground_truth_linear_gradient(left, right); self.vertex_gradient(ui, "Ground Truth (CPU gradient) - vertices", bg_fill, &g); - self.tex_gradient( - ui, - tex_allocator, - "Ground Truth (CPU gradient) - texture", - bg_fill, - &g, - ); + self.tex_gradient(ui, "Ground Truth (CPU gradient) - texture", bg_fill, &g); } else { let g = Gradient::ground_truth_linear_gradient(left, right).with_bg_fill(bg_fill); self.vertex_gradient( @@ -203,20 +182,13 @@ impl ColorTest { ); self.tex_gradient( ui, - tex_allocator, "Ground Truth (CPU gradient, CPU blending) - texture", bg_fill, &g, ); let g = Gradient::ground_truth_linear_gradient(left, right); self.vertex_gradient(ui, "CPU gradient, GPU blending - vertices", bg_fill, &g); - self.tex_gradient( - ui, - tex_allocator, - "CPU gradient, GPU blending - texture", - bg_fill, - &g, - ); + self.tex_gradient(ui, "CPU gradient, GPU blending - texture", bg_fill, &g); } let g = Gradient::texture_gradient(left, right); @@ -226,13 +198,7 @@ impl ColorTest { bg_fill, &g, ); - self.tex_gradient( - ui, - tex_allocator, - "Texture of width 2 (test texture sampler)", - bg_fill, - &g, - ); + self.tex_gradient(ui, "Texture of width 2 (test texture sampler)", bg_fill, &g); if self.srgb { let g = @@ -243,41 +209,26 @@ impl ColorTest { bg_fill, &g, ); - self.tex_gradient( - ui, - tex_allocator, - "Naive sRGBA interpolation (WRONG)", - bg_fill, - &g, - ); + self.tex_gradient(ui, "Naive sRGBA interpolation (WRONG)", bg_fill, &g); } }); } - fn tex_gradient( - &mut self, - ui: &mut Ui, - tex_allocator: Option<&dyn epi::TextureAllocator>, - label: &str, - bg_fill: Color32, - gradient: &Gradient, - ) { + fn tex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) { if !self.texture_gradients { return; } - if let Some(tex_allocator) = tex_allocator { - ui.horizontal(|ui| { - let tex = self.tex_mngr.get(tex_allocator, gradient); - 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)); - ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv)) - .on_hover_text(format!( - "A texture that is {} texels wide", - gradient.0.len() - )); - ui.label(label); - }); - } + ui.horizontal(|ui| { + let tex = self.tex_mngr.get(ui.ctx(), gradient); + 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)); + ui.add(Image::new(tex, GRADIENT_SIZE).bg_fill(bg_fill).uv(uv)) + .on_hover_text(format!( + "A texture that is {} texels wide", + gradient.0.len() + )); + ui.label(label); + }); } fn vertex_gradient(&mut self, ui: &mut Ui, label: &str, bg_fill: Color32, gradient: &Gradient) { @@ -384,18 +335,21 @@ impl Gradient { } #[derive(Default)] -struct TextureManager(HashMap); +struct TextureManager(HashMap); impl TextureManager { - fn get(&mut self, tex_allocator: &dyn epi::TextureAllocator, gradient: &Gradient) -> TextureId { - *self.0.entry(gradient.clone()).or_insert_with(|| { + fn get(&mut self, ctx: &egui::Context, gradient: &Gradient) -> &TextureHandle { + self.0.entry(gradient.clone()).or_insert_with(|| { let pixels = gradient.to_pixel_row(); let width = pixels.len(); let height = 1; - tex_allocator.alloc(epi::Image { - size: [width, height], - pixels, - }) + ctx.load_texture( + "color_test_gradient", + epaint::ColorImage { + size: [width, height], + pixels, + }, + ) }) } } diff --git a/egui_demo_lib/src/apps/demo/plot_demo.rs b/egui_demo_lib/src/apps/demo/plot_demo.rs index 01571218..4e55e4bd 100644 --- a/egui_demo_lib/src/apps/demo/plot_demo.rs +++ b/egui_demo_lib/src/apps/demo/plot_demo.rs @@ -306,9 +306,9 @@ impl Widget for &mut LegendDemo { } #[derive(PartialEq, Default)] -struct ItemsDemo {} - -impl ItemsDemo {} +struct ItemsDemo { + texture: Option, +} impl Widget for &mut ItemsDemo { fn ui(self, ui: &mut Ui) -> Response { @@ -343,12 +343,17 @@ impl Widget for &mut ItemsDemo { ); 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( - TextureId::Egui, + texture, Value::new(0.0, 10.0), [ - ui.fonts().font_image().width as f32 / 100.0, - ui.fonts().font_image().height as f32 / 100.0, + ui.fonts().font_image().width() as f32 / 100.0, + ui.fonts().font_image().height() as f32 / 100.0, ], ); diff --git a/egui_demo_lib/src/apps/demo/widget_gallery.rs b/egui_demo_lib/src/apps/demo/widget_gallery.rs index eb8c2c83..93561802 100644 --- a/egui_demo_lib/src/apps/demo/widget_gallery.rs +++ b/egui_demo_lib/src/apps/demo/widget_gallery.rs @@ -17,6 +17,8 @@ pub struct WidgetGallery { string: String, color: egui::Color32, animate_progress_bar: bool, + #[cfg_attr(feature = "serde", serde(skip))] + texture: Option, } impl Default for WidgetGallery { @@ -30,6 +32,7 @@ impl Default for WidgetGallery { string: Default::default(), color: egui::Color32::LIGHT_BLUE.linear_multiply(0.5), animate_progress_bar: false, + texture: None, } } } @@ -99,8 +102,14 @@ impl WidgetGallery { string, color, animate_progress_bar, + texture, } = 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.label("Welcome to the widget gallery!"); ui.end_row(); @@ -180,17 +189,14 @@ impl WidgetGallery { ui.color_edit_button_srgba(color); ui.end_row(); + let img_size = 16.0 * texture.size_vec2() / texture.size_vec2().y; + ui.add(doc_link_label("Image", "Image")); - ui.image(egui::TextureId::Egui, [24.0, 16.0]) - .on_hover_text("The egui font texture was the convenient choice to show here."); + ui.image(texture, img_size); ui.end_row(); ui.add(doc_link_label("ImageButton", "ImageButton")); - if ui - .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() - { + if ui.add(egui::ImageButton::new(texture, img_size)).clicked() { *boolean = !*boolean; } ui.end_row(); diff --git a/egui_demo_lib/src/apps/http_app.rs b/egui_demo_lib/src/apps/http_app.rs index 161118ea..7337d5d4 100644 --- a/egui_demo_lib/src/apps/http_app.rs +++ b/egui_demo_lib/src/apps/http_app.rs @@ -7,7 +7,7 @@ struct Resource { text: Option, /// If set, the response was an image. - image: Option, + image: Option, /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). colored_text: Option, @@ -17,7 +17,7 @@ impl Resource { fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self { let content_type = response.content_type().unwrap_or_default(); let image = if content_type.starts_with("image/") { - decode_image(&response.bytes) + load_image(&response.bytes).ok().map(|img| img.into()) } else { None }; @@ -112,7 +112,7 @@ impl epi::App for HttpApp { } else if let Some(result) = &self.result { match result { Ok(resource) => { - ui_resource(ui, frame, &mut self.tex_mngr, resource); + ui_resource(ui, &mut self.tex_mngr, resource); } Err(error) => { // 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 } -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 { response, 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(texture_id) = tex_mngr.texture(frame, &response.url, image) { - let mut size = egui::Vec2::new(image.size[0] as f32, image.size[1] as f32); - size *= (ui.available_width() / size.x).min(1.0); - ui.image(texture_id, size); - } + let texture = tex_mngr.texture(ui.ctx(), &response.url, image); + let mut size = texture.size_vec2(); + size *= (ui.available_width() / size.x).min(1.0); + ui.image(texture, size); } else if let Some(colored_text) = colored_text { colored_text.ui(ui); } else if let Some(text) = &text { @@ -293,33 +292,32 @@ impl ColoredText { #[derive(Default)] struct TexMngr { loaded_url: String, - texture_id: Option, + texture: Option, } impl TexMngr { fn texture( &mut self, - frame: &epi::Frame, + ctx: &egui::Context, url: &str, - image: &epi::Image, - ) -> Option { - if self.loaded_url != url { - if let Some(texture_id) = self.texture_id.take() { - frame.free_texture(texture_id); - } - - self.texture_id = Some(frame.alloc_texture(image.clone())); + image: &egui::ImageData, + ) -> &egui::TextureHandle { + if self.loaded_url != url || self.texture.is_none() { + self.texture = Some(ctx.load_texture(url, image.clone())); self.loaded_url = url.to_owned(); } - self.texture_id + self.texture.as_ref().unwrap() } } -fn decode_image(bytes: &[u8]) -> Option { - use image::GenericImageView; - let image = image::load_from_memory(bytes).ok()?; +fn load_image(image_data: &[u8]) -> Result { + 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 size = [image.width() as usize, image.height() as usize]; - let pixels = image_buffer.into_vec(); - Some(epi::Image::from_rgba_unmultiplied(size, &pixels)) + let pixels = image_buffer.as_flat_samples(); + Ok(egui::ColorImage::from_rgba_unmultiplied( + size, + pixels.as_slice(), + )) } diff --git a/egui_demo_lib/src/syntax_highlighting.rs b/egui_demo_lib/src/syntax_highlighting.rs index 9603f5ae..c8c08fb1 100644 --- a/egui_demo_lib/src/syntax_highlighting.rs +++ b/egui_demo_lib/src/syntax_highlighting.rs @@ -66,7 +66,7 @@ enum SyntectTheme { #[cfg(feature = "syntect")] impl SyntectTheme { - fn all() -> impl Iterator { + fn all() -> impl ExactSizeIterator { [ Self::Base16EightiesDark, Self::Base16MochaDark, diff --git a/egui_glium/CHANGELOG.md b/egui_glium/CHANGELOG.md index c01cd4ae..45cb1139 100644 --- a/egui_glium/CHANGELOG.md +++ b/egui_glium/CHANGELOG.md @@ -3,6 +3,8 @@ All notable changes to the `egui_glium` integration will be noted in this file. ## 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 diff --git a/egui_glium/Cargo.toml b/egui_glium/Cargo.toml index 8f6634bb..68900587 100644 --- a/egui_glium/Cargo.toml +++ b/egui_glium/Cargo.toml @@ -23,10 +23,15 @@ include = [ all-features = true [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"] } epi = { version = "0.16.0", path = "../epi", optional = true } +ahash = "0.7" +bytemuck = "1.7" glium = "0.31" [dev-dependencies] diff --git a/egui_glium/examples/native_texture.rs b/egui_glium/examples/native_texture.rs index 4c2b7a21..821c3c87 100644 --- a/egui_glium/examples/native_texture.rs +++ b/egui_glium/examples/native_texture.rs @@ -62,7 +62,7 @@ fn main() { let mut redraw = || { 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| { if ui .add(egui::Button::image_and_text( @@ -98,7 +98,7 @@ fn main() { // 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 diff --git a/egui_glium/examples/pure_glium.rs b/egui_glium/examples/pure_glium.rs index 0578f4c9..52808d7b 100644 --- a/egui_glium/examples/pure_glium.rs +++ b/egui_glium/examples/pure_glium.rs @@ -32,7 +32,7 @@ fn main() { let mut redraw = || { 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| { ui.heading("Hello World!"); if ui.button("Quit").clicked() { @@ -59,7 +59,7 @@ fn main() { // 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 diff --git a/egui_glium/src/epi_backend.rs b/egui_glium/src/epi_backend.rs index 001b6a66..3ad93b5b 100644 --- a/egui_glium/src/epi_backend.rs +++ b/egui_glium/src/epi_backend.rs @@ -66,11 +66,11 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { 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()); 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); } @@ -86,13 +86,12 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { &mut target, integration.egui_ctx.pixels_per_point(), clipped_meshes, - &integration.egui_ctx.font_image(), ); target.finish().unwrap(); } - for id in tex_allocation_data.destructions.drain(..) { + for id in textures_delta.free.drain(..) { painter.free_texture(id); } diff --git a/egui_glium/src/lib.rs b/egui_glium/src/lib.rs index 5adcd9dc..b8b7c647 100644 --- a/egui_glium/src/lib.rs +++ b/egui_glium/src/lib.rs @@ -104,6 +104,9 @@ pub struct EguiGlium { pub egui_ctx: egui::Context, pub egui_winit: egui_winit::State, pub painter: crate::Painter, + + shapes: Vec, + textures_delta: egui::TexturesDelta, } impl EguiGlium { @@ -112,6 +115,8 @@ impl EguiGlium { egui_ctx: Default::default(), egui_winit: egui_winit::State::new(display.gl_window().window()), 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) } - /// Returns `needs_repaint` and shapes to draw. - pub fn run( - &mut self, - display: &glium::Display, - run_ui: impl FnMut(&egui::Context), - ) -> (bool, Vec) { + /// Returns `true` if egui requests a repaint. + /// + /// Call [`Self::paint`] later to paint. + pub fn run(&mut self, display: &glium::Display, run_ui: impl FnMut(&egui::Context)) -> bool { let raw_input = self .egui_winit .take_egui_input(display.gl_window().window()); let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); let needs_repaint = egui_output.needs_repaint; - self.egui_winit - .handle_output(display.gl_window().window(), &self.egui_ctx, egui_output); - (needs_repaint, shapes) + let textures_delta = self.egui_winit.handle_output( + display.gl_window().window(), + &self.egui_ctx, + egui_output, + ); + + self.shapes = shapes; + self.textures_delta.append(textures_delta); + needs_repaint } - pub fn paint( - &mut self, - display: &glium::Display, - target: &mut T, - shapes: Vec, - ) { + /// Paint the results of the last call to [`Self::run`]. + pub fn paint(&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); + } + let clipped_meshes = self.egui_ctx.tessellate(shapes); self.painter.paint_meshes( display, target, self.egui_ctx.pixels_per_point(), clipped_meshes, - &self.egui_ctx.font_image(), ); + + for id in textures_delta.free.drain(..) { + self.painter.free_texture(id); + } } } diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index d6735d63..f003db04 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -2,10 +2,8 @@ #![allow(semicolon_in_expressions_from_macros)] // glium::program! macro use { - egui::{ - emath::Rect, - epaint::{Color32, Mesh}, - }, + ahash::AHashMap, + egui::{emath::Rect, epaint::Mesh}, glium::{ implement_vertex, index::PrimitiveType, @@ -14,19 +12,17 @@ use { uniform, uniforms::{MagnifySamplerFilter, SamplerWrapFunction}, }, - std::{collections::HashMap, rc::Rc}, + std::rc::Rc, }; pub struct Painter { program: glium::Program, - egui_texture: Option, - egui_texture_version: Option, - /// Index is the same as in [`egui::TextureId::User`]. - user_textures: HashMap>, + textures: AHashMap>, #[cfg(feature = "epi")] - next_native_tex_id: u64, // TODO: 128-bit texture space? + /// [`egui::TextureId::User`] index + next_native_tex_id: u64, } impl Painter { @@ -54,40 +50,12 @@ impl Painter { Painter { program, - egui_texture: None, - egui_texture_version: None, - user_textures: Default::default(), + textures: Default::default(), #[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> = 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. /// You should call `target.clear_color(..)` before /// and `target.finish()` after this. @@ -97,10 +65,7 @@ impl Painter { target: &mut T, pixels_per_point: f32, cipped_meshes: Vec, - font_image: &egui::FontImage, ) { - self.upload_egui_texture(display, font_image); - for egui::ClippedMesh(clip_rect, mesh) in cipped_meshes { self.paint_mesh(target, display, pixels_per_point, clip_rect, &mesh); } @@ -118,7 +83,8 @@ impl Painter { debug_assert!(mesh.is_valid()); let vertex_buffer = { - #[derive(Copy, Clone)] + #[repr(C)] + #[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] struct Vertex { a_pos: [f32; 2], a_tc: [f32; 2], @@ -126,18 +92,10 @@ impl Painter { } implement_vertex!(Vertex, a_pos, a_tc, a_srgba); - let vertices: Vec = mesh - .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(); + let vertices: &[Vertex] = bytemuck::cast_slice(&mesh.vertices); // 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. @@ -223,41 +181,48 @@ impl Painter { // ------------------------------------------------------------------------ - #[cfg(feature = "epi")] pub fn set_texture( &mut self, facade: &dyn glium::backend::Facade, - tex_id: u64, - image: &epi::Image, + tex_id: egui::TextureId, + image: &egui::ImageData, ) { - assert_eq!( - image.size[0] * image.size[1], - image.pixels.len(), - "Mismatch between texture size and texel count" - ); - - let pixels: Vec> = image - .pixels - .chunks(image.size[0] as usize) - .map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect()) - .collect(); - + let pixels: Vec<(u8, u8, u8, u8)> = match image { + egui::ImageData::Color(image) => { + assert_eq!( + image.width() * image.height(), + image.pixels.len(), + "Mismatch between texture size and texel count" + ); + image.pixels.iter().map(|color| color.to_tuple()).collect() + } + egui::ImageData::Alpha(image) => { + let gamma = 1.0; + image + .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 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) { - self.user_textures.remove(&tex_id); + pub fn free_texture(&mut self, tex_id: egui::TextureId) { + self.textures.remove(&tex_id); } fn get_texture(&self, texture_id: egui::TextureId) -> Option<&SrgbTexture2d> { - match texture_id { - egui::TextureId::Egui => self.egui_texture.as_ref(), - egui::TextureId::User(id) => self.user_textures.get(&id).map(|rc| rc.as_ref()), - } + self.textures.get(&texture_id).map(|rc| rc.as_ref()) } } @@ -266,15 +231,13 @@ impl epi::NativeTexture for Painter { type Texture = Rc; 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.user_textures.insert(id, native); - egui::TextureId::User(id as u64) + self.textures.insert(id, native); + id } fn replace_native_texture(&mut self, id: egui::TextureId, replacing: Self::Texture) { - if let egui::TextureId::User(id) = id { - self.user_textures.insert(id, replacing); - } + self.textures.insert(id, replacing); } } diff --git a/egui_glow/CHANGELOG.md b/egui_glow/CHANGELOG.md index c83a9c2e..6ff8ea75 100644 --- a/egui_glow/CHANGELOG.md +++ b/egui_glow/CHANGELOG.md @@ -3,9 +3,11 @@ All notable changes to the `egui_glow` integration will be noted in this file. ## 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]). * Fix failure to run in Chrome ((#1092)[https://github.com/emilk/egui/pull/1092]). + ## 0.16.0 - 2021-12-29 * 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)). diff --git a/egui_glow/Cargo.toml b/egui_glow/Cargo.toml index af53d9ac..429818a2 100644 --- a/egui_glow/Cargo.toml +++ b/egui_glow/Cargo.toml @@ -23,10 +23,13 @@ include = [ all-features = true [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" -epi = { version = "0.16.0", path = "../epi", optional = true } glow = "0.11" memoffset = "0.6" diff --git a/egui_glow/examples/pure_glow.rs b/egui_glow/examples/pure_glow.rs index e954fb2a..7fec582a 100644 --- a/egui_glow/examples/pure_glow.rs +++ b/egui_glow/examples/pure_glow.rs @@ -50,7 +50,7 @@ fn main() { let mut redraw = || { 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| { ui.heading("Hello World!"); if ui.button("Quit").clicked() { @@ -78,7 +78,7 @@ fn main() { // 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 diff --git a/egui_glow/src/epi_backend.rs b/egui_glow/src/epi_backend.rs index 8b48ee7a..8b6c6011 100644 --- a/egui_glow/src/epi_backend.rs +++ b/egui_glow/src/epi_backend.rs @@ -82,11 +82,11 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { 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()); 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); } @@ -99,7 +99,6 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { gl.clear_color(color[0], color[1], color[2], color[3]); gl.clear(glow::COLOR_BUFFER_BIT); } - painter.upload_egui_texture(&gl, &integration.egui_ctx.font_image()); painter.paint_meshes( &gl, gl_window.window().inner_size().into(), @@ -110,8 +109,8 @@ pub fn run(app: Box, native_options: &epi::NativeOptions) -> ! { gl_window.swap_buffers().unwrap(); } - for id in tex_allocation_data.destructions.drain(..) { - painter.free_texture(id); + for id in textures_delta.free.drain(..) { + painter.free_texture(&gl, id); } { diff --git a/egui_glow/src/lib.rs b/egui_glow/src/lib.rs index e4b9f2d7..113c48f2 100644 --- a/egui_glow/src/lib.rs +++ b/egui_glow/src/lib.rs @@ -112,6 +112,9 @@ pub struct EguiGlow { pub egui_ctx: egui::Context, pub egui_winit: egui_winit::State, pub painter: crate::Painter, + + shapes: Vec, + textures_delta: egui::TexturesDelta, } #[cfg(feature = "winit")] @@ -128,6 +131,8 @@ impl EguiGlow { eprintln!("some error occurred in initializing painter\n{}", error); }) .unwrap(), + shapes: Default::default(), + textures_delta: Default::default(), } } @@ -141,36 +146,51 @@ impl EguiGlow { 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( &mut self, window: &glutin::window::Window, run_ui: impl FnMut(&egui::Context), - ) -> (bool, Vec) { + ) -> bool { let raw_input = self.egui_winit.take_egui_input(window); let (egui_output, shapes) = self.egui_ctx.run(raw_input, run_ui); let needs_repaint = egui_output.needs_repaint; - self.egui_winit + let textures_delta = self + .egui_winit .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( &mut self, gl_window: &glutin::WindowedContext, gl: &glow::Context, - shapes: Vec, ) { + 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 dimensions: [u32; 2] = gl_window.window().inner_size().into(); - self.painter - .upload_egui_texture(gl, &self.egui_ctx.font_image()); self.painter.paint_meshes( gl, dimensions, self.egui_ctx.pixels_per_point(), clipped_meshes, ); + + for id in textures_delta.free.drain(..) { + self.painter.free_texture(gl, id); + } } /// Call to release the allocated graphics resources. diff --git a/egui_glow/src/misc_util.rs b/egui_glow/src/misc_util.rs index bd091f5c..293e3ab1 100644 --- a/egui_glow/src/misc_util.rs +++ b/egui_glow/src/misc_util.rs @@ -86,10 +86,6 @@ pub fn check_for_gl_error(gl: &glow::Context, context: &str) { } } -pub(crate) unsafe fn as_u8_slice(s: &[T]) -> &[u8] { - std::slice::from_raw_parts(s.as_ptr().cast::(), s.len() * std::mem::size_of::()) -} - pub(crate) fn glow_print(s: impl std::fmt::Display) { #[cfg(target_arch = "wasm32")] web_sys::console::log_1(&format!("egui_glow: {}", s).into()); diff --git a/egui_glow/src/painter.rs b/egui_glow/src/painter.rs index 5e087e63..b11b09c3 100644 --- a/egui_glow/src/painter.rs +++ b/egui_glow/src/painter.rs @@ -2,7 +2,6 @@ use std::collections::HashMap; -use bytemuck::cast_slice; use egui::{ emath::Rect, epaint::{Color32, Mesh, Vertex}, @@ -11,7 +10,7 @@ use glow::HasContext; use memoffset::offset_of; 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::shader_version::ShaderVersion; @@ -30,8 +29,6 @@ pub struct Painter { program: glow::Program, u_screen_size: glow::UniformLocation, u_sampler: glow::UniformLocation, - egui_texture: Option, - egui_texture_version: Option, is_webgl_1: bool, is_embedded: bool, vertex_array: crate::misc_util::VAO, @@ -42,8 +39,7 @@ pub struct Painter { vertex_buffer: glow::Buffer, element_array_buffer: glow::Buffer, - /// Index is the same as in [`egui::TextureId::User`]. - user_textures: HashMap, + textures: HashMap, #[cfg(feature = "epi")] next_native_tex_id: u64, // TODO: 128-bit texture space? @@ -212,8 +208,6 @@ impl Painter { program, u_screen_size, u_sampler, - egui_texture: None, - egui_texture_version: None, is_webgl_1, is_embedded: matches!(shader_version, ShaderVersion::Es100 | ShaderVersion::Es300), vertex_array, @@ -222,7 +216,7 @@ impl Painter { post_process, vertex_buffer, element_array_buffer, - user_textures: Default::default(), + textures: Default::default(), #[cfg(feature = "epi")] next_native_tex_id: 1 << 32, 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 = 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( &mut self, [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.buffer_data_u8_slice( glow::ARRAY_BUFFER, - as_u8_slice(mesh.vertices.as_slice()), + bytemuck::cast_slice(&mesh.vertices), glow::STREAM_DRAW, ); gl.bind_buffer(glow::ELEMENT_ARRAY_BUFFER, Some(self.element_array_buffer)); gl.buffer_data_u8_slice( glow::ELEMENT_ARRAY_BUFFER, - as_u8_slice(mesh.indices.as_slice()), + bytemuck::cast_slice(&mesh.indices), glow::STREAM_DRAW, ); @@ -425,52 +384,75 @@ impl Painter { // ------------------------------------------------------------------------ - #[cfg(feature = "epi")] - pub fn set_texture(&mut self, gl: &glow::Context, tex_id: u64, image: &epi::Image) { + pub fn set_texture( + &mut self, + gl: &glow::Context, + tex_id: egui::TextureId, + image: &egui::ImageData, + ) { self.assert_not_destroyed(); - assert_eq!( - image.size[0] * image.size[1], - image.pixels.len(), - "Mismatch between texture size and texel count" - ); + let gl_texture = 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] = cast_slice(image.pixels.as_ref()); + let data: &[u8] = bytemuck::cast_slice(image.pixels.as_ref()); - let gl_texture = srgb_texture2d( - gl, - self.is_webgl_1, - self.srgb_support, - self.texture_filter, - data, - image.size[0], - image.size[1], - ); + srgb_texture2d( + gl, + self.is_webgl_1, + self.srgb_support, + self.texture_filter, + data, + image.size[0], + 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 = image + .srgba_pixels(gamma) + .flat_map(|a| a.to_array()) + .collect(); - if let Some(old_tex) = self.user_textures.insert(tex_id, gl_texture) { - self.textures_to_destroy.push(old_tex); + srgb_texture2d( + 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) { - self.user_textures.remove(&tex_id); + pub fn free_texture(&mut self, gl: &glow::Context, tex_id: egui::TextureId) { + 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 { - self.assert_not_destroyed(); - - match texture_id { - egui::TextureId::Egui => self.egui_texture, - egui::TextureId::User(id) => self.user_textures.get(&id).copied(), - } + self.textures.get(&texture_id).copied() } unsafe fn destroy_gl(&self, gl: &glow::Context) { gl.delete_program(self.program); - if let Some(tex) = self.egui_texture { - gl.delete_texture(tex); - } - for tex in self.user_textures.values() { + for tex in self.textures.values() { gl.delete_texture(*tex); } 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 { self.assert_not_destroyed(); - - let id = self.next_native_tex_id; + let id = egui::TextureId::User(self.next_native_tex_id); self.next_native_tex_id += 1; - - self.user_textures.insert(id, native); - - egui::TextureId::User(id as u64) + self.textures.insert(id, native); + id } 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.user_textures.insert(id, replacing) { - self.textures_to_destroy.push(old_tex); - } + if let Some(old_tex) = self.textures.insert(id, replacing) { + self.textures_to_destroy.push(old_tex); } } } diff --git a/egui_glow/src/post_process.rs b/egui_glow/src/post_process.rs index 8fd76c6a..e12c1a0e 100644 --- a/egui_glow/src/post_process.rs +++ b/egui_glow/src/post_process.rs @@ -116,7 +116,7 @@ impl PostProcess { gl.bind_buffer(glow::ARRAY_BUFFER, Some(pos_buffer)); gl.buffer_data_u8_slice( glow::ARRAY_BUFFER, - crate::misc_util::as_u8_slice(&positions), + bytemuck::cast_slice(&positions), glow::STATIC_DRAW, ); diff --git a/egui_web/Cargo.toml b/egui_web/Cargo.toml index ae60cf96..1a3395b1 100644 --- a/egui_web/Cargo.toml +++ b/egui_web/Cargo.toml @@ -27,10 +27,13 @@ crate-type = ["cdylib", "rlib"] [dependencies] egui = { version = "0.16.0", path = "../egui", default-features = false, features = [ + "convert_bytemuck", "single_threaded", ] } egui_glow = { version = "0.16.0",path = "../egui_glow", default-features = false, optional = true } epi = { version = "0.16.0", path = "../epi" } + +bytemuck = "1.7" js-sys = "0.3" ron = { version = "0.7", optional = true } serde = { version = "1", optional = true } diff --git a/egui_web/src/backend.rs b/egui_web/src/backend.rs index fbeeeed3..eb473233 100644 --- a/egui_web/src/backend.rs +++ b/egui_web/src/backend.rs @@ -1,5 +1,6 @@ use crate::*; +use egui::TexturesDelta; pub use egui::{pos2, Color32}; // ---------------------------------------------------------------------------- @@ -92,7 +93,7 @@ pub struct AppRunner { screen_reader: crate::screen_reader::ScreenReader, pub(crate) text_cursor_pos: Option, pub(crate) mutable_text_under_cursor: bool, - pending_texture_destructions: Vec, + textures_delta: TexturesDelta, } impl AppRunner { @@ -139,7 +140,7 @@ impl AppRunner { screen_reader: Default::default(), text_cursor_pos: None, mutable_text_under_cursor: false, - pending_texture_destructions: Default::default(), + textures_delta: Default::default(), }; { @@ -183,7 +184,10 @@ impl AppRunner { Ok(()) } - pub fn logic(&mut self) -> Result<(egui::Output, Vec), JsValue> { + /// Returns `true` if egui requests a repaint. + /// + /// Call [`Self::paint`] later to paint + pub fn logic(&mut self) -> Result<(bool, Vec), JsValue> { let frame_start = now_sec(); 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); - 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(); @@ -205,32 +211,32 @@ impl AppRunner { window_title: _, // TODO: change title of window decorated: _, // Can't toggle decorations drag_window: _, // Can't be dragged - tex_allocation_data, } = 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); - 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) -> Result<(), JsValue> { - self.painter - .upload_egui_texture(&self.egui_ctx.font_image()); + let textures_delta = std::mem::take(&mut self.textures_delta); + for (id, image) in textures_delta.set { + self.painter.set_texture(id, image); + } + self.painter.clear(self.app.clear_color()); self.painter .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); } + 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 { self.screen_reader.speak(&output.events_description()); } @@ -243,27 +249,30 @@ impl AppRunner { events: _, // already handled mutable_text_under_cursor, text_cursor_pos, + textures_delta, } = output; - set_cursor_icon(*cursor_icon); + set_cursor_icon(cursor_icon); if let Some(open) = open_url { crate::open_url(&open.url, open.new_tab); } #[cfg(web_sys_unstable_apis)] if !copied_text.is_empty() { - set_clipboard_text(copied_text); + set_clipboard_text(&copied_text); } #[cfg(not(web_sys_unstable_apis))] 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()); - self.text_cursor_pos = *text_cursor_pos; + self.text_cursor_pos = text_cursor_pos; } + + textures_delta } } diff --git a/egui_web/src/glow_wrapping.rs b/egui_web/src/glow_wrapping.rs index 2d24a635..9a4721c9 100644 --- a/egui_web/src/glow_wrapping.rs +++ b/egui_web/src/glow_wrapping.rs @@ -1,5 +1,5 @@ use crate::{canvas_element_or_die, console_error}; -use egui::{ClippedMesh, FontImage, Rgba}; +use egui::{ClippedMesh, Rgba}; use egui_glow::glow; use wasm_bindgen::JsCast; use wasm_bindgen::JsValue; @@ -40,12 +40,12 @@ impl 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); } - fn free_texture(&mut self, tex_id: u64) { - self.painter.free_texture(tex_id); + fn free_texture(&mut self, tex_id: egui::TextureId) { + self.painter.free_texture(&self.glow_ctx, tex_id); } fn debug_info(&self) -> String { @@ -60,10 +60,6 @@ impl crate::Painter for WrappedGlowPainter { &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) { let canvas_dimension = [self.canvas.width(), self.canvas.height()]; egui_glow::painter::clear(&self.glow_ctx, canvas_dimension, clear_color) diff --git a/egui_web/src/lib.rs b/egui_web/src/lib.rs index 48d0f528..99dd305d 100644 --- a/egui_web/src/lib.rs +++ b/egui_web/src/lib.rs @@ -482,9 +482,9 @@ fn paint_and_schedule(runner_ref: AppRunnerRef) -> Result<(), JsValue> { fn paint_if_needed(runner_ref: &AppRunnerRef) -> Result<(), JsValue> { let mut runner_lock = runner_ref.0.lock(); 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)?; - if output.needs_repaint { + if needs_repaint { runner_lock.needs_repaint.set_true(); } runner_lock.auto_save(); @@ -1214,7 +1214,7 @@ fn is_mobile() -> Option { // candidate window moves following text element (agent), // so it appears that the IME candidate window moves with text cursor. // On mobile devices, there is no need to do that. -fn move_text_cursor(cursor: &Option, canvas_id: &str) -> Option<()> { +fn move_text_cursor(cursor: Option, canvas_id: &str) -> Option<()> { let style = text_agent().style(); // Note: movint agent on mobile devices will lead to unpredictable scroll. if is_mobile() == Some(false) { diff --git a/egui_web/src/painter.rs b/egui_web/src/painter.rs index 269761c8..8c246a4c 100644 --- a/egui_web/src/painter.rs +++ b/egui_web/src/painter.rs @@ -1,17 +1,15 @@ use wasm_bindgen::prelude::JsValue; 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; /// id of the canvas html element containing the rendering fn canvas_id(&self) -> &str; - fn upload_egui_texture(&mut self, font_image: &egui::FontImage); - fn clear(&mut self, clear_color: egui::Rgba); fn paint_meshes( diff --git a/egui_web/src/webgl1.rs b/egui_web/src/webgl1.rs index ce96a988..83617fa4 100644 --- a/egui_web/src/webgl1.rs +++ b/egui_web/src/webgl1.rs @@ -9,10 +9,7 @@ use { }, }; -use egui::{ - emath::vec2, - epaint::{Color32, FontImage}, -}; +use egui::{emath::vec2, epaint::Color32}; type Gl = WebGlRenderingContext; @@ -28,13 +25,8 @@ pub struct WebGlPainter { texture_format: u32, post_process: Option, - egui_texture: WebGlTexture, - egui_texture_version: Option, - - /// Index is the same as in [`egui::TextureId::User`]. - user_textures: HashMap, - - next_native_tex_id: u64, // TODO: 128-bit texture space? + textures: HashMap, + next_native_tex_id: u64, } impl WebGlPainter { @@ -101,18 +93,13 @@ impl WebGlPainter { color_buffer, texture_format, post_process, - egui_texture, - egui_texture_version: None, - user_textures: Default::default(), + textures: Default::default(), next_native_tex_id: 1 << 32, }) } fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> { - match texture_id { - egui::TextureId::Egui => Some(&self.egui_texture), - egui::TextureId::User(id) => self.user_textures.get(&id), - } + self.textures.get(&texture_id) } fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> { @@ -234,44 +221,8 @@ impl WebGlPainter { 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 = 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_texture = gl.create_texture().unwrap(); gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); @@ -291,20 +242,64 @@ impl crate::Painter for WebGlPainter { Gl::TEXTURE_2D, level, internal_format as _, - image.size[0] as _, - image.size[1] as _, + size[0] as _, + size[1] as _, border, src_format, src_type, - Some(&pixels), + Some(pixels), ) .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) { - self.user_textures.remove(&tex_id); + fn replace_native_texture(&mut self, id: egui::TextureId, texture: Self::Texture) { + 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 = 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 { @@ -323,48 +318,6 @@ impl crate::Painter for WebGlPainter { &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 = 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) { let gl = &self.gl; diff --git a/egui_web/src/webgl2.rs b/egui_web/src/webgl2.rs index b5f2e29b..8d0d5cca 100644 --- a/egui_web/src/webgl2.rs +++ b/egui_web/src/webgl2.rs @@ -10,10 +10,7 @@ use { }, }; -use egui::{ - emath::vec2, - epaint::{Color32, FontImage}, -}; +use egui::{emath::vec2, epaint::Color32}; type Gl = WebGl2RenderingContext; @@ -28,13 +25,8 @@ pub struct WebGl2Painter { color_buffer: WebGlBuffer, post_process: PostProcess, - egui_texture: WebGlTexture, - egui_texture_version: Option, - - /// Index is the same as in [`egui::TextureId::User`]. - user_textures: HashMap, - - next_native_tex_id: u64, // TODO: 128-bit texture space? + textures: HashMap, + next_native_tex_id: u64, } impl WebGl2Painter { @@ -85,18 +77,13 @@ impl WebGl2Painter { tc_buffer, color_buffer, post_process, - egui_texture, - egui_texture_version: None, - user_textures: Default::default(), + textures: Default::default(), next_native_tex_id: 1 << 32, }) } fn get_texture(&self, texture_id: egui::TextureId) -> Option<&WebGlTexture> { - match texture_id { - egui::TextureId::Egui => Some(&self.egui_texture), - egui::TextureId::User(id) => self.user_textures.get(&id), - } + self.textures.get(&texture_id) } fn paint_mesh(&self, mesh: &egui::epaint::Mesh16) -> Result<(), JsValue> { @@ -218,44 +205,8 @@ impl WebGl2Painter { 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 = 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_texture = gl.create_texture().unwrap(); gl.bind_texture(Gl::TEXTURE_2D, Some(&gl_texture)); @@ -271,24 +222,65 @@ impl crate::Painter for WebGl2Painter { 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 _, - image.size[0] as _, - image.size[1] as _, + internal_format as i32, + size[0] as i32, + size[1] as i32, border, src_format, src_type, - Some(&pixels), + Some(pixels), ) .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) { - self.user_textures.remove(&tex_id); + fn replace_native_texture(&mut self, id: egui::TextureId, native: Self::Texture) { + 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 = 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 { @@ -307,44 +299,6 @@ impl crate::Painter for WebGl2Painter { &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 = 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) { let gl = &self.gl; diff --git a/emath/src/align.rs b/emath/src/align.rs index 144a6eae..a5b26ec8 100644 --- a/emath/src/align.rs +++ b/emath/src/align.rs @@ -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::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); diff --git a/epaint/CHANGELOG.md b/epaint/CHANGELOG.md index 90b9522c..2c0d3410 100644 --- a/epaint/CHANGELOG.md +++ b/epaint/CHANGELOG.md @@ -4,8 +4,9 @@ All notable changes to the epaint crate will be documented in this file. ## Unreleased - * 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 * Anti-alias path ends ([#893](https://github.com/emilk/egui/pull/893)). diff --git a/epaint/src/image.rs b/epaint/src/image.rs new file mode 100644 index 00000000..67a4d846 --- /dev/null +++ b/epaint/src/image.rs @@ -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, +} + +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 { + /// 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 { + /// 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 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, +} + +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 + '_ { + let srgba_from_alpha_lut: Vec = (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 for ImageData { + #[inline(always)] + fn from(image: AlphaImage) -> Self { + Self::Alpha(image) + } +} diff --git a/epaint/src/lib.rs b/epaint/src/lib.rs index 3799cd4e..547d5340 100644 --- a/epaint/src/lib.rs +++ b/epaint/src/lib.rs @@ -88,6 +88,7 @@ #![allow(clippy::manual_range_contains)] pub mod color; +pub mod image; mod mesh; pub mod mutex; mod shadow; @@ -98,10 +99,13 @@ mod stroke; pub mod tessellator; pub mod text; mod texture_atlas; +mod texture_handle; +pub mod textures; pub mod util; pub use { color::{Color32, Rgba}, + image::{AlphaImage, ColorImage, ImageData}, mesh::{Mesh, Mesh16, Vertex}, shadow::Shadow, shape::{CircleShape, PathShape, RectShape, Shape, TextShape}, @@ -110,6 +114,8 @@ pub use { tessellator::{tessellate_shapes, TessellationOptions, Tessellator}, text::{Fonts, Galley, TextStyle}, texture_atlas::{FontImage, TextureAtlas}, + texture_handle::TextureHandle, + textures::TextureManager, }; 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); /// 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))] pub enum TextureId { - /// The egui font texture. - /// If you don't want to use a texture, pick this and the [`WHITE_UV`] for uv-coord. - Egui, + /// Textures allocated using [`TextureManager`]. + /// + /// The first texture (`TextureId::Epaint(0)`) is used for the font data. + Managed(u64), /// 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), } impl Default for TextureId { + /// The epaint font texture. fn default() -> Self { - Self::Egui + Self::Managed(0) } } diff --git a/epaint/src/mesh.rs b/epaint/src/mesh.rs index 78f1a9fb..800e6120 100644 --- a/epaint/src/mesh.rs +++ b/epaint/src/mesh.rs @@ -105,7 +105,7 @@ impl Mesh { #[inline(always)] 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 { pos, uv: WHITE_UV, @@ -168,7 +168,7 @@ impl Mesh { /// Uniformly colored rectangle. #[inline(always)] 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); } diff --git a/epaint/src/shape.rs b/epaint/src/shape.rs index c4236177..7e666b2e 100644 --- a/epaint/src/shape.rs +++ b/epaint/src/shape.rs @@ -149,7 +149,7 @@ impl Shape { if let Shape::Mesh(mesh) = self { mesh.texture_id } else { - super::TextureId::Egui + super::TextureId::default() } } diff --git a/epaint/src/text/font.rs b/epaint/src/text/font.rs index 67aa3e4b..6c180ff2 100644 --- a/epaint/src/text/font.rs +++ b/epaint/src/text/font.rs @@ -376,12 +376,12 @@ fn allocate_glyph( } else { let glyph_pos = atlas.allocate((glyph_width, glyph_height)); - let texture = atlas.image_mut(); + let image = atlas.image_mut(); glyph.draw(|x, y, v| { if v > 0.0 { let px = glyph_pos.0 + x 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; } }); diff --git a/epaint/src/text/fonts.rs b/epaint/src/text/fonts.rs index 07146254..df64169a 100644 --- a/epaint/src/text/fonts.rs +++ b/epaint/src/text/fonts.rs @@ -28,7 +28,7 @@ pub enum TextStyle { } impl TextStyle { - pub fn all() -> impl Iterator { + pub fn all() -> impl ExactSizeIterator { [ TextStyle::Small, 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`, // 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: let pos = atlas.allocate((1, 1)); assert_eq!(pos, (0, 0)); - atlas.image_mut()[pos] = 255; + atlas.image_mut().image[pos] = 255; } let atlas = Arc::new(Mutex::new(atlas)); @@ -287,7 +287,7 @@ impl Fonts { let mut atlas = atlas.lock(); let texture = atlas.image_mut(); // 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 { @@ -295,7 +295,7 @@ impl Fonts { definitions, fonts, atlas, - buffered_font_image: Default::default(), //atlas.lock().texture().clone(); + buffered_font_image: Default::default(), galley_cache: Default::default(), } } diff --git a/epaint/src/texture_atlas.rs b/epaint/src/texture_atlas.rs index a79501aa..08c6a067 100644 --- a/epaint/src/texture_atlas.rs +++ b/epaint/src/texture_atlas.rs @@ -1,59 +1,30 @@ +use crate::image::AlphaImage; + /// An 8-bit texture containing font data. #[derive(Clone, Default)] pub struct FontImage { /// e.g. a hash of the data. Use this to detect changes! /// If the texture changes, this too will change. pub version: u64, - pub width: usize, - pub height: usize, - /// 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, + + /// The actual image data. + pub image: AlphaImage, } impl FontImage { + #[inline] 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 + '_ { - use super::Color32; - - let srgba_from_luminance_lut: Vec = (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] - fn index(&self, (x, y): (usize, usize)) -> &u8 { - assert!(x < self.width); - assert!(y < self.height); - &self.pixels[y * self.width + x] + pub fn width(&self) -> usize { + self.image.size[0] } -} -impl std::ops::IndexMut<(usize, usize)> for FontImage { #[inline] - fn index_mut(&mut self, (x, y): (usize, usize)) -> &mut u8 { - assert!(x < self.width); - assert!(y < self.height); - &mut self.pixels[y * self.width + x] + pub fn height(&self) -> usize { + self.image.size[1] } } @@ -70,13 +41,11 @@ pub struct TextureAtlas { } impl TextureAtlas { - pub fn new(width: usize, height: usize) -> Self { + pub fn new(size: [usize; 2]) -> Self { Self { image: FontImage { version: 0, - width, - height, - pixels: vec![0; width * height], + image: AlphaImage::new(size), }, ..Default::default() } @@ -99,12 +68,12 @@ impl TextureAtlas { const PADDING: usize = 1; assert!( - w <= self.image.width, + w <= self.image.width(), "Tried to allocate a {} wide glyph in a {} wide texture atlas", 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: self.cursor.0 = 0; self.cursor.1 += self.row_height + PADDING; @@ -112,15 +81,7 @@ impl TextureAtlas { } self.row_height = self.row_height.max(h); - while self.cursor.1 + self.row_height >= self.image.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); - } + resize_to_min_height(&mut self.image.image, self.cursor.1 + self.row_height); let pos = self.cursor; self.cursor.0 += w + PADDING; @@ -128,3 +89,13 @@ impl TextureAtlas { (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); + } +} diff --git a/epaint/src/texture_handle.rs b/epaint/src/texture_handle.rs new file mode 100644 index 00000000..8cb37941 --- /dev/null +++ b/epaint/src/texture_handle.rs @@ -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>, + 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(&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>, 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) { + 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() + } +} diff --git a/epaint/src/textures.rs b/epaint/src/textures.rs new file mode 100644 index 00000000..79ef685f --- /dev/null +++ b/epaint/src/textures.rs @@ -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, + 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 { + 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, + + /// Texture to free after painting. + pub free: Vec, +} + +impl TexturesDelta { + pub fn append(&mut self, mut newer: TexturesDelta) { + self.set.extend(newer.set.into_iter()); + self.free.append(&mut newer.free); + } +} diff --git a/epi/src/lib.rs b/epi/src/lib.rs index c4a09f14..3b46494d 100644 --- a/epi/src/lib.rs +++ b/epi/src/lib.rs @@ -328,31 +328,7 @@ impl Frame { /// for integrations only: call once per frame pub fn take_app_output(&self) -> crate::backend::AppOutput { - let mut lock = self.lock(); - 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); + std::mem::take(&mut self.lock().output) } } @@ -386,41 +362,6 @@ pub struct IntegrationInfo { pub native_pixels_per_point: Option, } -/// 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, -} - -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 pub trait NativeTexture { /// 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`. pub mod backend { - use std::collections::HashMap; - use super::*; /// How to signal the [`egui`] integration that a repaint is required. @@ -498,49 +437,14 @@ pub mod backend { pub struct FrameData { /// Information about the integration. pub info: IntegrationInfo, + /// Where the app can issue commands back to the integration. pub output: AppOutput, + /// 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, } - /// 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, - /// destructions this frame. - pub destructions: Vec, - } - - 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. #[derive(Default)] #[must_use] @@ -560,8 +464,5 @@ pub mod backend { /// Set to true to drag window while primary mouse button is down. pub drag_window: bool, - - /// A way to allocate textures (on integrations that support it). - pub tex_allocation_data: TexAllocationData, } }