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:
@@ -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) {
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user