Texture loading in egui (#1110)

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

View File

@ -8,6 +8,7 @@ NOTE: [`epaint`](epaint/CHANGELOG.md), [`eframe`](eframe/CHANGELOG.md), [`egui_w
## Unreleased
### 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))

4
Cargo.lock generated
View File

@ -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",

View File

@ -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)).

View File

@ -4,7 +4,7 @@ use eframe::{egui, epi};
#[derive(Default)]
struct MyApp {
texture: Option<(egui::Vec2, egui::TextureId)>,
texture: Option<egui::TextureHandle>,
}
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<egui::ColorImage, image::ImageError> {
use image::GenericImageView as _;
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let pixels = image_buffer.as_flat_samples();
Ok(egui::ColorImage::from_rgba_unmultiplied(
size,
pixels.as_slice(),
))
}

View File

@ -55,10 +55,9 @@ pub fn handle_app_output(
window: &winit::window::Window,
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<egui::epaint::ClippedShape>,
) {
) -> (bool, egui::TexturesDelta, Vec<egui::epaint::ClippedShape>) {
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) {

View File

@ -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);

View File

@ -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<RwLock<epaint::TextureManager>>);
impl Default for WrappedTextureManager {
fn default() -> Self {
let mut tex_mngr = epaint::textures::TextureManager::default();
// Will be filled in later
let font_id = tex_mngr.alloc(
"egui_font_texture".into(),
epaint::AlphaImage::new([0, 0]).into(),
);
assert_eq!(font_id, TextureId::default());
Self(Arc::new(RwLock::new(tex_mngr)))
}
}
// ----------------------------------------------------------------------------
#[derive(Default)]
struct ContextImpl {
/// `None` until the start of the first frame.
fonts: Option<Fonts>,
memory: Memory,
animation_manager: AnimationManager,
latest_font_image_version: Option<u64>,
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<epaint::FontImage> {
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<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// Se also [`crate::ImageData`], [`crate::Ui::image`] and [`crate::ImageButton`].
pub fn load_texture(
&self,
name: impl Into<String>,
image: impl Into<ImageData>,
) -> TextureHandle {
let tex_mngr = self.tex_manager();
let tex_id = tex_mngr.write().alloc(name.into(), image.into());
TextureHandle::new(tex_mngr, tex_id)
}
/// Low-level texture manager.
///
/// In general it is easier to use [`Self::load_texture`] and [`TextureHandle`].
///
/// You can show stats about the allocated textures using [`Self::texture_ui`].
pub fn tex_manager(&self) -> Arc<RwLock<epaint::textures::TextureManager>> {
self.read().tex_manager.0.clone()
}
// ---------------------------------------------------------------------
/// Constrain the position of a window/area so it fits within the provided boundary.
@ -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) {

View File

@ -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");
});
}
}

View File

@ -35,6 +35,9 @@ pub struct Output {
/// Screen-space position of text edit cursor (used for IME).
pub text_cursor_pos: Option<crate::Pos2>,
/// 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)

View File

@ -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");
});
}
}

View File

@ -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)

View File

@ -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 {

View File

@ -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| {

View File

@ -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<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.image(texture, texture.size_vec2());
/// }
/// }
/// ```
///
/// Se also [`crate::Image`] and [`crate::ImageButton`].
#[inline]
pub fn image(&mut self, texture_id: TextureId, size: impl Into<Vec2>) -> Response {
pub fn image(&mut self, texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> Response {
Image::new(texture_id, size).ui(self)
}
}

View File

@ -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<Item = (f64, T)> + '_ {
pub fn iter(&'_ self) -> impl ExactSizeIterator<Item = (f64, T)> + '_ {
self.values.iter().map(|(time, value)| (*time, *value))
}
pub fn values(&'_ self) -> impl Iterator<Item = T> + '_ {
pub fn values(&'_ self) -> impl ExactSizeIterator<Item = T> + '_ {
self.values.iter().map(|(_time, value)| *value)
}

View File

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

View File

@ -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<egui::TextureHandle>,
/// }
///
/// impl MyImage {
/// fn ui(&mut self, ui: &mut egui::Ui) {
/// let texture: &egui::TextureHandle = self.texture.get_or_insert_with(|| {
/// // Load the texture only once.
/// ui.ctx().load_texture("my-image", egui::ColorImage::example())
/// });
///
/// // Show the image:
/// ui.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<Vec2>) -> Self {
pub fn new(texture_id: impl Into<TextureId>, size: impl Into<Vec2>) -> 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(),

View File

@ -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();

View File

@ -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<Vec2>) -> Self {
pub fn new(texture_id: impl Into<TextureId>, position: Value, size: impl Into<Vec2>) -> 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(),

View File

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

View File

@ -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<Gradient, TextureId>);
struct TextureManager(HashMap<Gradient, TextureHandle>);
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,
},
)
})
}
}

View File

@ -306,9 +306,9 @@ impl Widget for &mut LegendDemo {
}
#[derive(PartialEq, Default)]
struct ItemsDemo {}
impl ItemsDemo {}
struct ItemsDemo {
texture: Option<egui::TextureHandle>,
}
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,
],
);

View File

@ -17,6 +17,8 @@ pub struct WidgetGallery {
string: String,
color: egui::Color32,
animate_progress_bar: bool,
#[cfg_attr(feature = "serde", serde(skip))]
texture: Option<egui::TextureHandle>,
}
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();

View File

@ -7,7 +7,7 @@ struct Resource {
text: Option<String>,
/// If set, the response was an image.
image: Option<epi::Image>,
image: Option<egui::ImageData>,
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
colored_text: Option<ColoredText>,
@ -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<egui::TextureId>,
texture: Option<egui::TextureHandle>,
}
impl TexMngr {
fn texture(
&mut self,
frame: &epi::Frame,
ctx: &egui::Context,
url: &str,
image: &epi::Image,
) -> Option<egui::TextureId> {
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<epi::Image> {
use image::GenericImageView;
let image = image::load_from_memory(bytes).ok()?;
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
use image::GenericImageView as _;
let image = image::load_from_memory(image_data)?;
let size = [image.width() as _, image.height() as _];
let image_buffer = image.to_rgba8();
let 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(),
))
}

View File

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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -66,11 +66,11 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
std::thread::sleep(std::time::Duration::from_millis(10));
}
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<dyn epi::App>, 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);
}

View File

@ -104,6 +104,9 @@ pub struct EguiGlium {
pub egui_ctx: egui::Context,
pub egui_winit: egui_winit::State,
pub painter: crate::Painter,
shapes: Vec<egui::epaint::ClippedShape>,
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<egui::epaint::ClippedShape>) {
/// 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<T: glium::Surface>(
&mut self,
display: &glium::Display,
target: &mut T,
shapes: Vec<egui::epaint::ClippedShape>,
) {
/// Paint the results of the last call to [`Self::run`].
pub fn paint<T: glium::Surface>(&mut self, display: &glium::Display, target: &mut T) {
let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set {
self.painter.set_texture(display, id, &image);
}
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);
}
}
}

View File

@ -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<SrgbTexture2d>,
egui_texture_version: Option<u64>,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, Rc<SrgbTexture2d>>,
textures: AHashMap<egui::TextureId, Rc<SrgbTexture2d>>,
#[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<Vec<(u8, u8, u8, u8)>> = font_image
.pixels
.chunks(font_image.width as usize)
.map(|row| {
row.iter()
.map(|&a| Color32::from_white_alpha(a).to_tuple())
.collect()
})
.collect();
let format = texture::SrgbFormat::U8U8U8U8;
let mipmaps = texture::MipmapsOption::NoMipmap;
self.egui_texture =
Some(SrgbTexture2d::with_format(facade, pixels, format, mipmaps).unwrap());
self.egui_texture_version = Some(font_image.version);
}
/// Main entry-point for painting a frame.
/// 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<egui::ClippedMesh>,
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<Vertex> = 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<Vec<(u8, u8, u8, u8)>> = 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<SrgbTexture2d>;
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);
}
}

View File

@ -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)).

View File

@ -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"

View File

@ -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

View File

@ -82,11 +82,11 @@ pub fn run(app: Box<dyn epi::App>, native_options: &epi::NativeOptions) -> ! {
std::thread::sleep(std::time::Duration::from_millis(10));
}
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<dyn epi::App>, 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<dyn epi::App>, 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);
}
{

View File

@ -112,6 +112,9 @@ pub struct EguiGlow {
pub egui_ctx: egui::Context,
pub egui_winit: egui_winit::State,
pub painter: crate::Painter,
shapes: Vec<egui::epaint::ClippedShape>,
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<egui::epaint::ClippedShape>) {
) -> 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<glutin::PossiblyCurrent>,
gl: &glow::Context,
shapes: Vec<egui::epaint::ClippedShape>,
) {
let shapes = std::mem::take(&mut self.shapes);
let mut textures_delta = std::mem::take(&mut self.textures_delta);
for (id, image) in textures_delta.set {
self.painter.set_texture(gl, id, &image);
}
let clipped_meshes = self.egui_ctx.tessellate(shapes);
let 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.

View File

@ -86,10 +86,6 @@ pub fn check_for_gl_error(gl: &glow::Context, context: &str) {
}
}
pub(crate) unsafe fn as_u8_slice<T>(s: &[T]) -> &[u8] {
std::slice::from_raw_parts(s.as_ptr().cast::<u8>(), s.len() * std::mem::size_of::<T>())
}
pub(crate) fn glow_print(s: impl std::fmt::Display) {
#[cfg(target_arch = "wasm32")]
web_sys::console::log_1(&format!("egui_glow: {}", s).into());

View File

@ -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<glow::Texture>,
egui_texture_version: Option<u64>,
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<u64, glow::Texture>,
textures: HashMap<egui::TextureId, glow::Texture>,
#[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<u8> = font_image
.srgba_pixels(gamma)
.flat_map(|a| Vec::from(a.to_array()))
.collect();
if let Some(old_tex) = std::mem::replace(
&mut self.egui_texture,
Some(srgb_texture2d(
gl,
self.is_webgl_1,
self.srgb_support,
self.texture_filter,
&pixels,
font_image.width,
font_image.height,
)),
) {
unsafe {
gl.delete_texture(old_tex);
}
}
self.egui_texture_version = Some(font_image.version);
}
unsafe fn prepare_painting(
&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<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
if let Some(old_tex) = self.user_textures.insert(tex_id, gl_texture) {
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<glow::Texture> {
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);
}
}
}

View File

@ -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,
);

View File

@ -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 }

View File

@ -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<egui::Pos2>,
pub(crate) mutable_text_under_cursor: bool,
pending_texture_destructions: Vec<u64>,
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<egui::ClippedMesh>), JsValue> {
/// Returns `true` if egui requests a repaint.
///
/// Call [`Self::paint`] later to paint
pub fn logic(&mut self) -> Result<(bool, Vec<egui::ClippedMesh>), JsValue> {
let frame_start = now_sec();
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<egui::ClippedMesh>) -> 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
}
}

View File

@ -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)

View File

@ -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<bool> {
// 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<egui::Pos2>, canvas_id: &str) -> Option<()> {
fn move_text_cursor(cursor: Option<egui::Pos2>, 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) {

View File

@ -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(

View File

@ -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<PostProcess>,
egui_texture: WebGlTexture,
egui_texture_version: Option<u64>,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, WebGlTexture>,
next_native_tex_id: u64, // TODO: 128-bit texture space?
textures: HashMap<egui::TextureId, WebGlTexture>,
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<u8> = Vec::with_capacity(image.pixels.len() * 4);
for srgba in image.pixels {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) {
let gl = &self.gl;
let gl_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<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
self.set_texture_rgba(tex_id, image.size, &data);
}
};
}
fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id);
}
fn debug_info(&self) -> String {
@ -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<u8> = Vec::with_capacity(font_image.pixels.len() * 4);
for srgba in font_image.srgba_pixels(gamma) {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
let gl = &self.gl;
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture));
let level = 0;
let internal_format = self.texture_format;
let border = 0;
let src_format = self.texture_format;
let src_type = Gl::UNSIGNED_BYTE;
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
level,
internal_format as i32,
font_image.width as i32,
font_image.height as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
self.egui_texture_version = Some(font_image.version);
}
fn clear(&mut self, clear_color: egui::Rgba) {
let gl = &self.gl;

View File

@ -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<u64>,
/// Index is the same as in [`egui::TextureId::User`].
user_textures: HashMap<u64, WebGlTexture>,
next_native_tex_id: u64, // TODO: 128-bit texture space?
textures: HashMap<egui::TextureId, WebGlTexture>,
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<u8> = Vec::with_capacity(image.pixels.len() * 4);
for srgba in image.pixels {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
fn set_texture_rgba(&mut self, tex_id: egui::TextureId, size: [usize; 2], pixels: &[u8]) {
let gl = &self.gl;
let gl_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<u8> = image
.srgba_pixels(gamma)
.flat_map(|a| a.to_array())
.collect();
self.set_texture_rgba(tex_id, image.size, &data);
}
};
}
fn free_texture(&mut self, tex_id: egui::TextureId) {
self.textures.remove(&tex_id);
}
fn debug_info(&self) -> String {
@ -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<u8> = Vec::with_capacity(font_image.pixels.len() * 4);
for srgba in font_image.srgba_pixels(1.0) {
pixels.push(srgba.r());
pixels.push(srgba.g());
pixels.push(srgba.b());
pixels.push(srgba.a());
}
let gl = &self.gl;
gl.bind_texture(Gl::TEXTURE_2D, Some(&self.egui_texture));
let level = 0;
let internal_format = Gl::SRGB8_ALPHA8;
let border = 0;
let src_format = Gl::RGBA;
let src_type = Gl::UNSIGNED_BYTE;
gl.pixel_storei(Gl::UNPACK_ALIGNMENT, 1);
gl.tex_image_2d_with_i32_and_i32_and_i32_and_format_and_type_and_opt_u8_array(
Gl::TEXTURE_2D,
level,
internal_format as i32,
font_image.width as i32,
font_image.height as i32,
border,
src_format,
src_type,
Some(&pixels),
)
.unwrap();
self.egui_texture_version = Some(font_image.version);
}
fn clear(&mut self, clear_color: egui::Rgba) {
let gl = &self.gl;

View File

@ -47,7 +47,7 @@ impl Align {
}
}
/// ``` rust
/// ```
/// assert_eq!(emath::Align::Min.align_size_within_range(2.0, 10.0..=20.0), 10.0..=12.0);
/// assert_eq!(emath::Align::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);

View File

@ -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)).

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

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

View File

@ -88,6 +88,7 @@
#![allow(clippy::manual_range_contains)]
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)
}
}

View File

@ -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);
}

View File

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

View File

@ -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;
}
});

View File

@ -28,7 +28,7 @@ pub enum TextStyle {
}
impl TextStyle {
pub fn all() -> impl Iterator<Item = TextStyle> {
pub fn all() -> impl ExactSizeIterator<Item = TextStyle> {
[
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(),
}
}

View File

@ -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<u8>,
/// 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<Item = super::Color32> + '_ {
use super::Color32;
let srgba_from_luminance_lut: Vec<Color32> = (0..=255)
.map(|a| {
let a = super::color::linear_f32_from_linear_u8(a).powf(gamma);
super::Rgba::from_white_alpha(a).into()
})
.collect();
self.pixels
.iter()
.map(move |&l| srgba_from_luminance_lut[l as usize])
}
}
impl std::ops::Index<(usize, usize)> for FontImage {
type Output = u8;
#[inline]
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);
}
}

View File

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

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

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

View File

@ -328,31 +328,7 @@ impl Frame {
/// for integrations only: call once per frame
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<f32>,
}
/// How to allocate textures (images) to use in [`egui`].
pub trait TextureAllocator {
/// Allocate a new user texture.
///
/// There is no way to change a texture.
/// Instead allocate a new texture and free the previous one with [`Self::free`].
fn alloc(&self, image: Image) -> egui::TextureId;
/// Free the given texture.
fn free(&self, id: egui::TextureId);
}
/// A 2D color image in RAM.
#[derive(Clone, Default)]
pub struct Image {
/// width, height
pub size: [usize; 2],
/// The pixels, row by row, from top to bottom.
pub pixels: Vec<egui::Color32>,
}
impl Image {
/// Create an `Image` from flat RGBA data.
/// Panics unless `size[0] * size[1] * 4 == rgba.len()`.
/// This is usually what you want to use after having loaded an image.
pub fn from_rgba_unmultiplied(size: [usize; 2], rgba: &[u8]) -> Self {
assert_eq!(size[0] * size[1] * 4, rgba.len());
let pixels = rgba
.chunks_exact(4)
.map(|p| egui::Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3]))
.collect();
Self { size, pixels }
}
}
/// Abstraction for platform dependent texture reference
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<dyn RepaintSignal>,
}
/// The data needed in order to allocate and free textures/images.
#[derive(Default)]
#[must_use]
pub struct TexAllocationData {
/// We allocate texture id linearly.
pub(crate) next_id: u64,
/// New creations this frame
pub creations: HashMap<u64, Image>,
/// destructions this frame.
pub destructions: Vec<u64>,
}
impl TexAllocationData {
/// Should only be used by integrations
pub fn take(&mut self) -> Self {
let next_id = self.next_id;
let ret = std::mem::take(self);
self.next_id = next_id;
ret
}
/// Allocate a new texture.
pub fn alloc(&mut self, image: Image) -> egui::TextureId {
let id = self.next_id;
self.next_id += 1;
self.creations.insert(id, image);
egui::TextureId::User(id)
}
/// Free an existing texture.
pub fn free(&mut self, id: egui::TextureId) {
if let egui::TextureId::User(id) = id {
self.destructions.push(id);
}
}
}
/// Action that can be taken by the user app.
#[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,
}
}