1
0
mirror of https://github.com/emilk/egui.git synced 2026-09-02 06:40:06 -04:00

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
59 changed files with 1297 additions and 860 deletions

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,