From 99a2a525103f3f246b91310851516b4306592a97 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 20 Nov 2020 19:50:47 +0100 Subject: [PATCH] Code cleanup --- egui/src/paint/texture_atlas.rs | 12 +++ egui_glium/src/painter.rs | 148 ++++++++++++++++---------------- egui_web/src/webgl.rs | 3 +- example_web/src/example_app.rs | 141 ++++++++++++++++-------------- 4 files changed, 167 insertions(+), 137 deletions(-) diff --git a/egui/src/paint/texture_atlas.rs b/egui/src/paint/texture_atlas.rs index 546f21c2..cabd86f4 100644 --- a/egui/src/paint/texture_atlas.rs +++ b/egui/src/paint/texture_atlas.rs @@ -6,9 +6,21 @@ pub struct Texture { pub version: u64, pub width: usize, pub height: usize, + /// luminance and alpha, linear space 0-255 pub pixels: Vec, } +impl Texture { + /// Returns the textures as `sRGBA` premultiplied pixels, row by row, top to bottom. + pub fn srgba_pixels<'slf>(&'slf self) -> impl Iterator + 'slf { + use super::Srgba; + let srgba_from_luminance_lut: Vec = (0..=255).map(Srgba::white_alpha).collect(); + self.pixels + .iter() + .map(move |&l| srgba_from_luminance_lut[l as usize]) + } +} + impl std::ops::Index<(usize, usize)> for Texture { type Output = u8; diff --git a/egui_glium/src/painter.rs b/egui_glium/src/painter.rs index 3e225a18..81f9d4f6 100644 --- a/egui_glium/src/painter.rs +++ b/egui_glium/src/painter.rs @@ -95,64 +95,6 @@ impl Painter { } } - pub fn alloc_user_texture(&mut self) -> egui::TextureId { - for (i, tex) in self.user_textures.iter_mut().enumerate() { - if tex.is_none() { - *tex = Some(Default::default()); - return egui::TextureId::User(i as u64); - } - } - let id = egui::TextureId::User(self.user_textures.len() as u64); - self.user_textures.push(Some(Default::default())); - id - } - - pub fn set_user_texture( - &mut self, - id: egui::TextureId, - size: (usize, usize), - pixels: &[Srgba], - ) { - assert_eq!(size.0 * size.1, pixels.len()); - - if let egui::TextureId::User(id) = id { - if let Some(user_texture) = self.user_textures.get_mut(id as usize) { - if let Some(user_texture) = user_texture { - let pixels: Vec> = pixels - .chunks(size.0 as usize) - .map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect()) - .collect(); - - *user_texture = UserTexture { - pixels, - gl_texture: None, - }; - } - } - } - } - - pub fn free_user_texture(&mut self, id: egui::TextureId) { - if let egui::TextureId::User(id) = id { - let index = id as usize; - if index < self.user_textures.len() { - self.user_textures[index] = None; - } - } - } - - 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 as usize)? - .as_ref()? - .gl_texture - .as_ref(), - } - } - fn upload_egui_texture( &mut self, facade: &dyn glium::backend::Facade, @@ -179,20 +121,6 @@ impl Painter { self.egui_texture_version = Some(texture.version); } - fn upload_pending_user_textures(&mut self, facade: &dyn glium::backend::Facade) { - for user_texture in &mut self.user_textures { - if let Some(user_texture) = user_texture { - if user_texture.gl_texture.is_none() { - let pixels = std::mem::take(&mut user_texture.pixels); - let format = texture::SrgbFormat::U8U8U8U8; - let mipmaps = texture::MipmapsOption::NoMipmap; - user_texture.gl_texture = - Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap()); - } - } - } - } - /// Main entry-point for painting a frame pub fn paint_jobs( &mut self, @@ -326,4 +254,80 @@ impl Painter { .unwrap(); } } + + // ------------------------------------------------------------------------ + // user textures: this is an experimental feature. + // No need to implement this in your Egui integration! + + pub fn alloc_user_texture(&mut self) -> egui::TextureId { + for (i, tex) in self.user_textures.iter_mut().enumerate() { + if tex.is_none() { + *tex = Some(Default::default()); + return egui::TextureId::User(i as u64); + } + } + let id = egui::TextureId::User(self.user_textures.len() as u64); + self.user_textures.push(Some(Default::default())); + id + } + + pub fn set_user_texture( + &mut self, + id: egui::TextureId, + size: (usize, usize), + pixels: &[Srgba], + ) { + assert_eq!(size.0 * size.1, pixels.len()); + + if let egui::TextureId::User(id) = id { + if let Some(user_texture) = self.user_textures.get_mut(id as usize) { + if let Some(user_texture) = user_texture { + let pixels: Vec> = pixels + .chunks(size.0 as usize) + .map(|row| row.iter().map(|srgba| srgba.to_tuple()).collect()) + .collect(); + + *user_texture = UserTexture { + pixels, + gl_texture: None, + }; + } + } + } + } + + pub fn free_user_texture(&mut self, id: egui::TextureId) { + if let egui::TextureId::User(id) = id { + let index = id as usize; + if index < self.user_textures.len() { + self.user_textures[index] = None; + } + } + } + + 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 as usize)? + .as_ref()? + .gl_texture + .as_ref(), + } + } + + fn upload_pending_user_textures(&mut self, facade: &dyn glium::backend::Facade) { + for user_texture in &mut self.user_textures { + if let Some(user_texture) = user_texture { + if user_texture.gl_texture.is_none() { + let pixels = std::mem::take(&mut user_texture.pixels); + let format = texture::SrgbFormat::U8U8U8U8; + let mipmaps = texture::MipmapsOption::NoMipmap; + user_texture.gl_texture = + Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap()); + } + } + } + } } diff --git a/egui_web/src/webgl.rs b/egui_web/src/webgl.rs index b30098b8..fba5f40c 100644 --- a/egui_web/src/webgl.rs +++ b/egui_web/src/webgl.rs @@ -234,8 +234,7 @@ impl Painter { } let mut pixels: Vec = Vec::with_capacity(texture.pixels.len() * 4); - for &alpha in &texture.pixels { - let srgba = Srgba::white_alpha(alpha); + for srgba in texture.srgba_pixels() { pixels.push(srgba.r()); pixels.push(srgba.g()); pixels.push(srgba.b()); diff --git a/example_web/src/example_app.rs b/example_web/src/example_app.rs index 00c2031b..3f093c3a 100644 --- a/example_web/src/example_app.rs +++ b/example_web/src/example_app.rs @@ -1,28 +1,6 @@ use egui_web::fetch::Response; use std::sync::mpsc::Receiver; -struct Image { - size: (usize, usize), - pixels: Vec, -} - -impl Image { - fn decode(bytes: &[u8]) -> Option { - use image::GenericImageView; - let image = image::load_from_memory(&bytes).ok()?; - let image_buffer = image.to_rgba(); - let size = (image.width() as usize, image.height() as usize); - let pixels = image_buffer.into_vec(); - assert_eq!(size.0 * size.1 * 4, pixels.len()); - let pixels = pixels - .chunks(4) - .map(|p| egui::Srgba::from_rgba_unmultiplied(p[0], p[1], p[2], p[3])) - .collect(); - - Some(Image { size, pixels }) - } -} - struct Resource { /// HTTP response response: Response, @@ -31,11 +9,22 @@ struct Resource { image: Option, } +impl Resource { + fn from_response(response: Response) -> Self { + let image = if response.header_content_type.starts_with("image/") { + Image::decode(&response.bytes) + } else { + None + }; + Self { response, image } + } +} + pub struct ExampleApp { url: String, in_progress: Option>>, result: Option>, - texture_id: Option, + tex_mngr: TexMngr, } impl Default for ExampleApp { @@ -44,7 +33,7 @@ impl Default for ExampleApp { url: "https://raw.githubusercontent.com/emilk/egui/master/README.md".to_owned(), in_progress: Default::default(), result: Default::default(), - texture_id: None, + tex_mngr: Default::default(), } } } @@ -58,7 +47,7 @@ impl egui::app::App for ExampleApp { integration_context: &mut egui::app::IntegrationContext, ) { egui::CentralPanel::default().show(ctx, |ui| { - ui.heading("HTTP Get inside of Egui"); + ui.heading("Egui Example App"); ui.add(egui::github_link_file!( "https://github.com/emilk/egui/blob/master/", "(source code)" @@ -81,7 +70,7 @@ impl egui::app::App for ExampleApp { } else if let Some(result) = &self.result { match result { Ok(resource) => { - ui_resouce(ui, self.texture_id, resource); + ui_resouce(ui, integration_context, &mut self.tex_mngr, resource); } Err(error) => { // This should only happen if the fetch API isn't available or something similar. @@ -91,40 +80,11 @@ impl egui::app::App for ExampleApp { } }); - self.poll_receiver(integration_context); - } -} - -impl ExampleApp { - fn load_image( - &mut self, - integration_context: &mut egui::app::IntegrationContext, - response: &Response, - ) -> Option { - let tex_allocator = integration_context.tex_allocator.as_mut()?; - - if matches!( - response.header_content_type.as_str(), - "image/jpeg" | "image/png" - ) { - let image = Image::decode(&response.bytes)?; - let texture_id = self.texture_id.unwrap_or_else(|| tex_allocator.alloc()); - self.texture_id = Some(texture_id); - tex_allocator.set_srgba_premultiplied(texture_id, image.size, &image.pixels); - return Some(image); - } - None - } - - fn poll_receiver(&mut self, integration_context: &mut egui::app::IntegrationContext) { if let Some(receiver) = &mut self.in_progress { // Are we there yet? if let Ok(result) = receiver.try_recv() { self.in_progress = None; - self.result = Some(result.map(|response| Resource { - image: self.load_image(integration_context, &response), - response, - })); + self.result = Some(result.map(Resource::from_response)); } } } @@ -136,7 +96,10 @@ fn ui_url(ui: &mut egui::Ui, url: &mut String) -> bool { ui.horizontal(|ui| { ui.label("URL:"); trigger_fetch |= ui.text_edit_singleline(url).lost_kb_focus; + trigger_fetch |= ui.button("GET").clicked; + }); + ui.horizontal(|ui| { if ui.button("Source code for this example").clicked { *url = format!( "https://raw.githubusercontent.com/emilk/egui/master/{}", @@ -152,12 +115,16 @@ fn ui_url(ui: &mut egui::Ui, url: &mut String) -> bool { trigger_fetch = true; } }); - trigger_fetch |= ui.button("GET").clicked; trigger_fetch } -fn ui_resouce(ui: &mut egui::Ui, texture_id: Option, resource: &Resource) { +fn ui_resouce( + ui: &mut egui::Ui, + integration_context: &mut egui::app::IntegrationContext, + tex_mngr: &mut TexMngr, + resource: &Resource, +) { let Resource { response, image } = resource; ui.monospace(format!("url: {}", response.url)); @@ -172,11 +139,9 @@ fn ui_resouce(ui: &mut egui::Ui, texture_id: Option, resource: )); if let Some(image) = image { - if let Some(texture_id) = texture_id { - ui.image( - texture_id, - egui::Vec2::new(image.size.0 as f32, image.size.1 as f32), - ); + if let Some(texture_id) = tex_mngr.texture(integration_context, &response.url, &image) { + let size = egui::Vec2::new(image.size.0 as f32, image.size.1 as f32); + ui.image(texture_id, size); } } else if let Some(text) = &response.text { ui.monospace("Body:"); @@ -188,3 +153,53 @@ fn ui_resouce(ui: &mut egui::Ui, texture_id: Option, resource: ui.monospace("[binary]"); } } + +// ---------------------------------------------------------------------------- +// Texture/image handling is very manual at the moment. + +/// Immediate mode texture managers that supports at most one texture at the time :) +#[derive(Default)] +struct TexMngr { + loaded_url: String, + texture_id: Option, +} + +impl TexMngr { + fn texture( + &mut self, + integration_context: &mut egui::app::IntegrationContext, + url: &str, + image: &Image, + ) -> Option { + let tex_allocator = integration_context.tex_allocator.as_mut()?; + let texture_id = self.texture_id.unwrap_or_else(|| tex_allocator.alloc()); + self.texture_id = Some(texture_id); + if self.loaded_url != url { + self.loaded_url = url.to_owned(); + tex_allocator.set_srgba_premultiplied(texture_id, image.size, &image.pixels); + } + Some(texture_id) + } +} + +struct Image { + size: (usize, usize), + pixels: Vec, +} + +impl Image { + fn decode(bytes: &[u8]) -> Option { + use image::GenericImageView; + let image = image::load_from_memory(&bytes).ok()?; + let image_buffer = image.to_rgba(); + let size = (image.width() as usize, image.height() as usize); + let pixels = image_buffer.into_vec(); + assert_eq!(size.0 * size.1 * 4, pixels.len()); + let pixels = pixels + .chunks(4) + .map(|p| egui::Srgba::from_rgba_unmultiplied(p[0], p[1], p[2], p[3])) + .collect(); + + Some(Image { size, pixels }) + } +}