mirror of
https://github.com/emilk/egui.git
synced 2026-08-30 13:20:05 -04:00
Introduce egui_extras with RetainedImage for loading svg,png,jpeg,… (#1282)
This commit is contained in:
177
egui_extras/src/image.rs
Normal file
177
egui_extras/src/image.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
use parking_lot::Mutex;
|
||||
|
||||
/// An image to be shown in egui.
|
||||
///
|
||||
/// Load once, and save somewhere in your app state.
|
||||
///
|
||||
/// Use the `svg` and `image` features to enable more constructors.
|
||||
pub struct RetainedImage {
|
||||
debug_name: String,
|
||||
size: [usize; 2],
|
||||
/// Cleared once [`Self::texture`] has been loaded.
|
||||
image: Mutex<egui::ColorImage>,
|
||||
/// Lazily loaded when we have an egui context.
|
||||
texture: Mutex<Option<egui::TextureHandle>>,
|
||||
}
|
||||
|
||||
impl RetainedImage {
|
||||
pub fn from_color_image(debug_name: impl Into<String>, image: ColorImage) -> Self {
|
||||
Self {
|
||||
debug_name: debug_name.into(),
|
||||
size: image.size,
|
||||
image: Mutex::new(image),
|
||||
texture: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load a (non-svg) image.
|
||||
///
|
||||
/// Requires the "image" feature. You must also opt-in to the image formats you need
|
||||
/// with e.g. `image = { version = "0.24", features = ["jpeg", "png"] }`.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image or unsupported image format.
|
||||
#[cfg(feature = "image")]
|
||||
pub fn from_image_bytes(
|
||||
debug_name: impl Into<String>,
|
||||
image_bytes: &[u8],
|
||||
) -> Result<Self, String> {
|
||||
Ok(Self::from_color_image(
|
||||
debug_name,
|
||||
load_image_bytes(image_bytes)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Pass in the bytes of an SVG that you've loaded.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn from_svg_bytes(debug_name: impl Into<String>, svg_bytes: &[u8]) -> Result<Self, String> {
|
||||
Ok(Self::from_color_image(
|
||||
debug_name,
|
||||
load_svg_bytes(svg_bytes)?,
|
||||
))
|
||||
}
|
||||
|
||||
/// Pass in the str of an SVG that you've loaded.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn from_svg_str(debug_name: impl Into<String>, svg_str: &str) -> Result<Self, String> {
|
||||
Self::from_svg_bytes(debug_name, svg_str.as_bytes())
|
||||
}
|
||||
|
||||
/// The size of the image data (number of pixels wide/high).
|
||||
pub fn size(&self) -> [usize; 2] {
|
||||
self.size
|
||||
}
|
||||
|
||||
/// The size of the image data (number of pixels wide/high).
|
||||
pub fn size_vec2(&self) -> egui::Vec2 {
|
||||
let [w, h] = self.size();
|
||||
egui::vec2(w as f32, h as f32)
|
||||
}
|
||||
|
||||
/// The debug name of the image, e.g. the file name.
|
||||
pub fn debug_name(&self) -> &str {
|
||||
&self.debug_name
|
||||
}
|
||||
|
||||
/// The texture if for this image.
|
||||
pub fn texture_id(&self, ctx: &egui::Context) -> egui::TextureId {
|
||||
self.texture
|
||||
.lock()
|
||||
.get_or_insert_with(|| {
|
||||
let image: &mut ColorImage = &mut self.image.lock();
|
||||
let image = std::mem::take(image);
|
||||
ctx.load_texture(&self.debug_name, image)
|
||||
})
|
||||
.id()
|
||||
}
|
||||
|
||||
/// Show the image with the given maximum size.
|
||||
pub fn show_max_size(&self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response {
|
||||
let mut desired_size = self.size_vec2();
|
||||
desired_size *= (max_size.x / desired_size.x).min(1.0);
|
||||
desired_size *= (max_size.y / desired_size.y).min(1.0);
|
||||
self.show_size(ui, desired_size)
|
||||
}
|
||||
|
||||
/// Show the image with the original size (one image pixel = one gui point).
|
||||
pub fn show(&self, ui: &mut egui::Ui) -> egui::Response {
|
||||
self.show_size(ui, self.size_vec2())
|
||||
}
|
||||
|
||||
/// Show the image with the given scale factor (1.0 = original size).
|
||||
pub fn show_scaled(&self, ui: &mut egui::Ui, scale: f32) -> egui::Response {
|
||||
self.show_size(ui, self.size_vec2() * scale)
|
||||
}
|
||||
|
||||
/// Show the image with the given size.
|
||||
pub fn show_size(&self, ui: &mut egui::Ui, desired_size: egui::Vec2) -> egui::Response {
|
||||
// We need to convert the SVG to a texture to display it:
|
||||
// Future improvement: tell backend to do mip-mapping of the image to
|
||||
// make it look smoother when downsized.
|
||||
ui.image(self.texture_id(ui.ctx()), desired_size)
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
use egui::ColorImage;
|
||||
|
||||
/// Load a (non-svg) image.
|
||||
///
|
||||
/// Requires the "image" feature. You must also opt-in to the image formats you need
|
||||
/// with e.g. `image = { version = "0.24", features = ["jpeg", "png"] }`.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image or unsupported image format.
|
||||
#[cfg(feature = "image")]
|
||||
pub fn load_image_bytes(image_bytes: &[u8]) -> Result<egui::ColorImage, String> {
|
||||
let image = image::load_from_memory(image_bytes).map_err(|err| err.to_string())?;
|
||||
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(),
|
||||
))
|
||||
}
|
||||
|
||||
/// Load an SVG and rasterize it into an egui image.
|
||||
///
|
||||
/// Requires the "svg" feature.
|
||||
///
|
||||
/// # Errors
|
||||
/// On invalid image
|
||||
#[cfg(feature = "svg")]
|
||||
pub fn load_svg_bytes(svg_bytes: &[u8]) -> Result<egui::ColorImage, String> {
|
||||
let mut opt = usvg::Options::default();
|
||||
opt.fontdb.load_system_fonts();
|
||||
|
||||
let rtree = usvg::Tree::from_data(svg_bytes, &opt.to_ref()).map_err(|err| err.to_string())?;
|
||||
|
||||
let pixmap_size = rtree.svg_node().size.to_screen_size();
|
||||
let [w, h] = [pixmap_size.width(), pixmap_size.height()];
|
||||
|
||||
let mut pixmap = tiny_skia::Pixmap::new(w, h)
|
||||
.ok_or_else(|| format!("Failed to create SVG Pixmap of size {}x{}", w, h))?;
|
||||
|
||||
resvg::render(
|
||||
&rtree,
|
||||
usvg::FitTo::Original,
|
||||
tiny_skia::Transform::default(),
|
||||
pixmap.as_mut(),
|
||||
)
|
||||
.ok_or_else(|| "Failed to render SVG".to_owned())?;
|
||||
|
||||
let image = egui::ColorImage::from_rgba_unmultiplied(
|
||||
[pixmap.width() as _, pixmap.height() as _],
|
||||
pixmap.data(),
|
||||
);
|
||||
|
||||
Ok(image)
|
||||
}
|
||||
87
egui_extras/src/lib.rs
Normal file
87
egui_extras/src/lib.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! This is a crate that adds some features on top top of [`egui`](https://github.com/emilk/egui). This crate are for experimental features, and features that require big dependencies that does not belong in `egui`.
|
||||
|
||||
// Forbid warnings in release builds:
|
||||
#![cfg_attr(not(debug_assertions), deny(warnings))]
|
||||
#![forbid(unsafe_code)]
|
||||
#![warn(
|
||||
clippy::all,
|
||||
clippy::await_holding_lock,
|
||||
clippy::char_lit_as_u8,
|
||||
clippy::checked_conversions,
|
||||
clippy::dbg_macro,
|
||||
clippy::debug_assert_with_mut_call,
|
||||
clippy::disallowed_method,
|
||||
clippy::doc_markdown,
|
||||
clippy::empty_enum,
|
||||
clippy::enum_glob_use,
|
||||
clippy::exit,
|
||||
clippy::expl_impl_clone_on_copy,
|
||||
clippy::explicit_deref_methods,
|
||||
clippy::explicit_into_iter_loop,
|
||||
clippy::fallible_impl_from,
|
||||
clippy::filter_map_next,
|
||||
clippy::flat_map_option,
|
||||
clippy::float_cmp_const,
|
||||
clippy::fn_params_excessive_bools,
|
||||
clippy::from_iter_instead_of_collect,
|
||||
clippy::if_let_mutex,
|
||||
clippy::implicit_clone,
|
||||
clippy::imprecise_flops,
|
||||
clippy::inefficient_to_string,
|
||||
clippy::invalid_upcast_comparisons,
|
||||
clippy::large_digit_groups,
|
||||
clippy::large_stack_arrays,
|
||||
clippy::large_types_passed_by_value,
|
||||
clippy::let_unit_value,
|
||||
clippy::linkedlist,
|
||||
clippy::lossy_float_literal,
|
||||
clippy::macro_use_imports,
|
||||
clippy::manual_ok_or,
|
||||
clippy::map_err_ignore,
|
||||
clippy::map_flatten,
|
||||
clippy::map_unwrap_or,
|
||||
clippy::match_on_vec_items,
|
||||
clippy::match_same_arms,
|
||||
clippy::match_wild_err_arm,
|
||||
clippy::match_wildcard_for_single_variants,
|
||||
clippy::mem_forget,
|
||||
clippy::mismatched_target_os,
|
||||
clippy::missing_errors_doc,
|
||||
clippy::missing_safety_doc,
|
||||
clippy::mut_mut,
|
||||
clippy::mutex_integer,
|
||||
clippy::needless_borrow,
|
||||
clippy::needless_continue,
|
||||
clippy::needless_for_each,
|
||||
clippy::needless_pass_by_value,
|
||||
clippy::option_option,
|
||||
clippy::path_buf_push_overwrite,
|
||||
clippy::ptr_as_ptr,
|
||||
clippy::ref_option_ref,
|
||||
clippy::rest_pat_in_fully_bound_structs,
|
||||
clippy::same_functions_in_if_condition,
|
||||
clippy::semicolon_if_nothing_returned,
|
||||
clippy::single_match_else,
|
||||
clippy::string_add_assign,
|
||||
clippy::string_add,
|
||||
clippy::string_lit_as_bytes,
|
||||
clippy::string_to_string,
|
||||
clippy::todo,
|
||||
clippy::trait_duplication_in_bounds,
|
||||
clippy::unimplemented,
|
||||
clippy::unnested_or_patterns,
|
||||
clippy::unused_self,
|
||||
clippy::useless_transmute,
|
||||
clippy::verbose_file_reads,
|
||||
clippy::zero_sized_map_values,
|
||||
future_incompatible,
|
||||
nonstandard_style,
|
||||
rust_2018_idioms,
|
||||
rustdoc::missing_crate_level_docs
|
||||
)]
|
||||
#![allow(clippy::float_cmp)]
|
||||
#![allow(clippy::manual_range_contains)]
|
||||
|
||||
pub mod image;
|
||||
|
||||
pub use crate::image::RetainedImage;
|
||||
Reference in New Issue
Block a user