Introduce egui_extras with RetainedImage for loading svg,png,jpeg,… (#1282)

This commit is contained in:
Emil Ernerfeldt 2022-02-21 15:26:26 +01:00 committed by GitHub
parent 713917e481
commit c3fc8997d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 453 additions and 176 deletions

View file

@ -132,7 +132,7 @@ jobs:
toolchain: 1.56.0 toolchain: 1.56.0
override: true override: true
- run: sudo apt-get update && sudo apt-get install libspeechd-dev - run: sudo apt-get update && sudo apt-get install libspeechd-dev
- run: cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui-winit -p egui_glium -p egui_glow --lib --no-deps --all-features - run: cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui-winit -p egui_extras -p egui_glium -p egui_glow --lib --no-deps --all-features
doc_web: doc_web:
name: cargo doc web name: cargo doc web

View file

@ -5,7 +5,7 @@ Also see [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUT
## Crate overview ## Crate overview
The crates in this repository are: `egui, emath, epaint, egui, epi, egui-winit, egui_web, egui_glium, egui_glow, egui_demo_lib, egui_demo_app`. The crates in this repository are: `egui, emath, epaint, egui_extras, epi, egui-winit, egui_web, egui_glium, egui_glow, egui_demo_lib, egui_demo_app`.
### `egui`: The main GUI library. ### `egui`: The main GUI library.
Example code: `if ui.button("Click me").clicked() { … }` Example code: `if ui.button("Click me").clicked() { … }`
@ -21,6 +21,9 @@ Example: `Shape::Circle { center, radius, fill, stroke }`
Depends on `emath`, [`ab_glyph`](https://crates.io/crates/ab_glyph), [`atomic_refcell`](https://crates.io/crates/atomic_refcell), [`ahash`](https://crates.io/crates/ahash). Depends on `emath`, [`ab_glyph`](https://crates.io/crates/ab_glyph), [`atomic_refcell`](https://crates.io/crates/atomic_refcell), [`ahash`](https://crates.io/crates/ahash).
### `egui_extras`
This adds additional features on top of `egui`.
### `epi` ### `epi`
Depends only on `egui`. Depends only on `egui`.
Adds a thin application level wrapper around `egui` for hosting an `egui` app inside of `eframe`. Adds a thin application level wrapper around `egui` for hosting an `egui` app inside of `eframe`.

94
Cargo.lock generated
View file

@ -994,6 +994,7 @@ version = "0.16.0"
dependencies = [ dependencies = [
"egui", "egui",
"egui-winit", "egui-winit",
"egui_extras",
"egui_glium", "egui_glium",
"egui_glow", "egui_glow",
"egui_web", "egui_web",
@ -1001,10 +1002,7 @@ dependencies = [
"epi", "epi",
"image", "image",
"poll-promise", "poll-promise",
"resvg",
"rfd", "rfd",
"tiny-skia",
"usvg",
] ]
[[package]] [[package]]
@ -1053,6 +1051,7 @@ dependencies = [
"chrono", "chrono",
"criterion", "criterion",
"egui", "egui",
"egui_extras",
"ehttp", "ehttp",
"enum-map", "enum-map",
"epi", "epi",
@ -1063,6 +1062,18 @@ dependencies = [
"unicode_names2", "unicode_names2",
] ]
[[package]]
name = "egui_extras"
version = "0.16.0"
dependencies = [
"egui",
"image",
"parking_lot",
"resvg",
"tiny-skia",
"usvg",
]
[[package]] [[package]]
name = "egui_glium" name = "egui_glium"
version = "0.16.0" version = "0.16.0"
@ -1335,14 +1346,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]] [[package]]
name = "fontdb" name = "fontconfig-parser"
version = "0.7.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b07f5c05414a0d8caba4c17eef8dc8b5c8955fc7c68d324191c7a56d3f3449" checksum = "82cea2adebf32a9b104b8ffb308b5fb3b456f04cc76c294c3c85025c8a5d75f4"
dependencies = [ dependencies = [
"roxmltree",
]
[[package]]
name = "fontdb"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db856ee8cca3b9f23dd11c13bf3d4854b663ae86ed0c4a627a354431fc265f66"
dependencies = [
"fontconfig-parser",
"log", "log",
"memmap2 0.5.2", "memmap2 0.5.2",
"ttf-parser 0.12.3", "ttf-parser 0.15.0",
] ]
[[package]] [[package]]
@ -1515,6 +1536,16 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "gif"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a7187e78088aead22ceedeee99779455b23fc231fe13ec443f99bb71694e5b"
dependencies = [
"color_quant",
"weezl",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.26.1" version = "0.26.1"
@ -1764,7 +1795,7 @@ dependencies = [
"bytemuck", "bytemuck",
"byteorder", "byteorder",
"color_quant", "color_quant",
"jpeg-decoder 0.2.1", "jpeg-decoder",
"num-iter", "num-iter",
"num-rational", "num-rational",
"num-traits", "num-traits",
@ -1836,15 +1867,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]] [[package]]
name = "jpeg-decoder" name = "jpeg-decoder"
version = "0.1.22" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" checksum = "105fb082d64e2100074587f59a74231f771750c664af903f1f9f76c9dedfc6f1"
[[package]]
name = "jpeg-decoder"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbcf0244f6597be39ab8d9203f574cafb529ae8c698afa2182f7b3c3205a4a9c"
[[package]] [[package]]
name = "js-sys" name = "js-sys"
@ -2671,11 +2696,12 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]] [[package]]
name = "resvg" name = "resvg"
version = "0.20.0" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d94a32ca845cdda27237a40beba9bd3d3858ac8fc5356eb9442bdeecfe34d9e0" checksum = "2e702d1e8e00a3a0717b96244cba840f34f542d8f23097c8903266c4e2975658"
dependencies = [ dependencies = [
"jpeg-decoder 0.1.22", "gif",
"jpeg-decoder",
"log", "log",
"pico-args", "pico-args",
"png", "png",
@ -2801,14 +2827,14 @@ dependencies = [
[[package]] [[package]]
name = "rustybuzz" name = "rustybuzz"
version = "0.4.0" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44561062e583c4873162861261f16fd1d85fe927c4904d71329a4fe43dc355ef" checksum = "25ff94f20221325d000e552781713e53b0d85c1d9551b6f420d12daf5a08eace"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"bytemuck", "bytemuck",
"smallvec", "smallvec",
"ttf-parser 0.12.3", "ttf-parser 0.15.0",
"unicode-bidi-mirroring", "unicode-bidi-mirroring",
"unicode-ccc", "unicode-ccc",
"unicode-general-category", "unicode-general-category",
@ -3339,18 +3365,18 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "ttf-parser"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6"
[[package]] [[package]]
name = "ttf-parser" name = "ttf-parser"
version = "0.14.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281"
[[package]]
name = "ttf-parser"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c74c96594835e10fa545e2a51e8709f30b173a092bfd6036ef2cec53376244f3"
[[package]] [[package]]
name = "tts" name = "tts"
version = "0.20.2" version = "0.20.2"
@ -3472,9 +3498,9 @@ dependencies = [
[[package]] [[package]]
name = "usvg" name = "usvg"
version = "0.20.0" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00f064d38f79ff69e3160e2fba884e4ede897061c15178041a3976371c68cab1" checksum = "a261d60a7215fa339482047cc3dafd4e22e2bf34396aaebef2b707355bbb39c0"
dependencies = [ dependencies = [
"base64", "base64",
"data-url", "data-url",
@ -3490,7 +3516,7 @@ dependencies = [
"simplecss", "simplecss",
"siphasher", "siphasher",
"svgtypes", "svgtypes",
"ttf-parser 0.12.3", "ttf-parser 0.15.0",
"unicode-bidi", "unicode-bidi",
"unicode-script", "unicode-script",
"unicode-vo", "unicode-vo",
@ -3733,6 +3759,12 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "weezl"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e"
[[package]] [[package]]
name = "wepoll-ffi" name = "wepoll-ffi"
version = "0.1.2" version = "0.1.2"

View file

@ -3,6 +3,7 @@ resolver = "2"
members = [ members = [
"egui_demo_app", "egui_demo_app",
"egui_demo_lib", "egui_demo_lib",
"egui_extras",
"egui_glium", "egui_glium",
"egui_glow", "egui_glow",
"egui_web", "egui_web",

View file

@ -64,12 +64,9 @@ egui_web = { version = "0.16.0", path = "../egui_web", default-features = false,
[dev-dependencies] [dev-dependencies]
# For examples:
egui_extras = { path = "../egui_extras", features = ["image", "svg"] }
ehttp = "0.2" ehttp = "0.2"
image = { version = "0.24", default-features = false, features = ["jpeg", "png"] } image = { version = "0.24", default-features = false, features = ["jpeg", "png"] }
poll-promise = "0.1" poll-promise = "0.1"
rfd = "0.7" rfd = "0.7"
# svg.rs example:
resvg = "0.20"
tiny-skia = "0.6"
usvg = "0.20"

View file

@ -1,6 +1,7 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi}; use eframe::{egui, epi};
use egui_extras::RetainedImage;
use poll_promise::Promise; use poll_promise::Promise;
fn main() { fn main() {
@ -11,7 +12,7 @@ fn main() {
#[derive(Default)] #[derive(Default)]
struct MyApp { struct MyApp {
/// `None` when download hasn't started yet. /// `None` when download hasn't started yet.
promise: Option<Promise<ehttp::Result<egui::TextureHandle>>>, promise: Option<Promise<ehttp::Result<RetainedImage>>>,
} }
impl epi::App for MyApp { impl epi::App for MyApp {
@ -24,14 +25,13 @@ impl epi::App for MyApp {
// Begin download. // Begin download.
// We download the image using `ehttp`, a library that works both in WASM and on native. // We download the image using `ehttp`, a library that works both in WASM and on native.
// We use the `poll-promise` library to communicate with the UI thread. // We use the `poll-promise` library to communicate with the UI thread.
let ctx = ctx.clone();
let frame = frame.clone(); let frame = frame.clone();
let (sender, promise) = Promise::new(); let (sender, promise) = Promise::new();
let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024"); let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024");
ehttp::fetch(request, move |response| { ehttp::fetch(request, move |response| {
let image = response.and_then(parse_response);
sender.send(image); // send the results back to the UI thread.
frame.request_repaint(); // wake up UI thread frame.request_repaint(); // wake up UI thread
let texture = response.and_then(|response| parse_response(&ctx, response));
sender.send(texture); // send the results back to the UI thread.
}); });
promise promise
}); });
@ -43,24 +43,17 @@ impl epi::App for MyApp {
Some(Err(err)) => { Some(Err(err)) => {
ui.colored_label(egui::Color32::RED, err); // something went wrong ui.colored_label(egui::Color32::RED, err); // something went wrong
} }
Some(Ok(texture)) => { Some(Ok(image)) => {
let mut size = texture.size_vec2(); image.show_max_size(ui, ui.available_size());
size *= (ui.available_width() / size.x).min(1.0);
size *= (ui.available_height() / size.y).min(1.0);
ui.image(texture, size);
} }
}); });
} }
} }
fn parse_response( fn parse_response(response: ehttp::Response) -> Result<RetainedImage, String> {
ctx: &egui::Context,
response: ehttp::Response,
) -> Result<egui::TextureHandle, String> {
let content_type = response.content_type().unwrap_or_default(); let content_type = response.content_type().unwrap_or_default();
if content_type.starts_with("image/") { if content_type.starts_with("image/") {
let image = load_image(&response.bytes).map_err(|err| err.to_string())?; RetainedImage::from_image_bytes(&response.url, &response.bytes)
Ok(ctx.load_texture("my-image", image))
} else { } else {
Err(format!( Err(format!(
"Expected image, found content-type {:?}", "Expected image, found content-type {:?}",
@ -68,14 +61,3 @@ fn parse_response(
)) ))
} }
} }
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
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

@ -1,10 +1,22 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi}; use eframe::{egui, epi};
use egui_extras::RetainedImage;
#[derive(Default)]
struct MyApp { struct MyApp {
texture: Option<egui::TextureHandle>, image: RetainedImage,
}
impl Default for MyApp {
fn default() -> Self {
Self {
image: RetainedImage::from_image_bytes(
"rust-logo-256x256.png",
include_bytes!("rust-logo-256x256.png"),
)
.unwrap(),
}
}
} }
impl epi::App for MyApp { impl epi::App for MyApp {
@ -13,17 +25,15 @@ impl epi::App for MyApp {
} }
fn update(&mut self, ctx: &egui::Context, _frame: &epi::Frame) { 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| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("This is an image:"); ui.heading("This is an image:");
ui.image(texture, texture.size_vec2()); self.image.show(ui);
ui.heading("This is an image you can click:"); ui.heading("This is an image you can click:");
ui.add(egui::ImageButton::new(texture, texture.size_vec2())); ui.add(egui::ImageButton::new(
self.image.texture_id(ctx),
self.image.size_vec2(),
));
}); });
} }
} }
@ -32,14 +42,3 @@ fn main() {
let options = eframe::NativeOptions::default(); let options = eframe::NativeOptions::default();
eframe::run_native(Box::new(MyApp::default()), options); eframe::run_native(Box::new(MyApp::default()), options);
} }
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
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

@ -1,82 +1,23 @@
//! A good way of displaying an SVG image in egui. //! A good way of displaying an SVG image in egui.
//! //!
//! Requires the dependencies `resvg`, `tiny-skia`, `usvg` //! Requires the dependency `egui_extras` with the `svg` feature.
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::{egui, epi}; use eframe::{egui, epi};
/// Load an SVG and rasterize it into an egui image.
fn load_svg_data(svg_data: &[u8]) -> Result<egui::ColorImage, String> {
let mut opt = usvg::Options::default();
opt.fontdb.load_system_fonts();
let rtree = usvg::Tree::from_data(svg_data, &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)
}
// ----------------------------------------------------------------------------
/// An SVG image to be shown in egui
struct SvgImage {
image: egui::ColorImage,
texture: Option<egui::TextureHandle>,
}
impl SvgImage {
/// Pass itn the bytes of an SVG that you've loaded from disk
pub fn from_svg_data(bytes: &[u8]) -> Result<Self, String> {
Ok(Self {
image: load_svg_data(bytes)?,
texture: None,
})
}
pub fn show_max_size(&mut self, ui: &mut egui::Ui, max_size: egui::Vec2) -> egui::Response {
let mut desired_size = egui::vec2(self.image.width() as _, self.image.height() as _);
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)
}
pub fn show_size(&mut 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.
let svg_texture = self
.texture
.get_or_insert_with(|| ui.ctx().load_texture("svg", self.image.clone()));
ui.image(svg_texture, desired_size)
}
}
// ----------------------------------------------------------------------------
struct MyApp { struct MyApp {
svg_image: SvgImage, svg_image: egui_extras::RetainedImage,
} }
impl Default for MyApp { impl Default for MyApp {
fn default() -> Self { fn default() -> Self {
Self { Self {
svg_image: SvgImage::from_svg_data(include_bytes!("rustacean-flat-happy.svg")).unwrap(), svg_image: egui_extras::RetainedImage::from_svg_bytes(
"rustacean-flat-happy.svg",
include_bytes!("rustacean-flat-happy.svg"),
)
.unwrap(),
} }
} }
} }

View file

@ -51,6 +51,13 @@ impl From<&String> for RichText {
} }
} }
impl From<&mut String> for RichText {
#[inline]
fn from(text: &mut String) -> Self {
RichText::new(text.clone())
}
}
impl From<String> for RichText { impl From<String> for RichText {
#[inline] #[inline]
fn from(text: String) -> Self { fn from(text: String) -> Self {

View file

@ -40,6 +40,7 @@ syntax_highlighting = ["syntect"]
[dependencies] [dependencies]
egui = { version = "0.16.0", path = "../egui", default-features = false } egui = { version = "0.16.0", path = "../egui", default-features = false }
egui_extras = { version = "0.16.0", path = "../egui_extras", features = ["image"] }
epi = { version = "0.16.0", path = "../epi" } epi = { version = "0.16.0", path = "../epi" }
chrono = { version = "0.4", features = ["js-sys", "wasmbind"], optional = true } chrono = { version = "0.4", features = ["js-sys", "wasmbind"], optional = true }

View file

@ -1,3 +1,4 @@
use egui_extras::RetainedImage;
use poll_promise::Promise; use poll_promise::Promise;
struct Resource { struct Resource {
@ -7,7 +8,7 @@ struct Resource {
text: Option<String>, text: Option<String>,
/// If set, the response was an image. /// If set, the response was an image.
texture: Option<egui::TextureHandle>, image: Option<RetainedImage>,
/// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md").
colored_text: Option<ColoredText>, colored_text: Option<ColoredText>,
@ -17,13 +18,11 @@ impl Resource {
fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self { fn from_response(ctx: &egui::Context, response: ehttp::Response) -> Self {
let content_type = response.content_type().unwrap_or_default(); let content_type = response.content_type().unwrap_or_default();
let image = if content_type.starts_with("image/") { let image = if content_type.starts_with("image/") {
load_image(&response.bytes).ok() RetainedImage::from_image_bytes(&response.url, &response.bytes).ok()
} else { } else {
None None
}; };
let texture = image.map(|image| ctx.load_texture(&response.url, image));
let text = response.text(); let text = response.text();
let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text));
let text = text.map(|text| text.to_owned()); let text = text.map(|text| text.to_owned());
@ -31,7 +30,7 @@ impl Resource {
Self { Self {
response, response,
text, text,
texture, image,
colored_text, colored_text,
} }
} }
@ -151,7 +150,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
let Resource { let Resource {
response, response,
text, text,
texture, image,
colored_text, colored_text,
} = resource; } = resource;
@ -198,10 +197,10 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) {
ui.separator(); ui.separator();
} }
if let Some(texture) = texture { if let Some(image) = image {
let mut size = texture.size_vec2(); let mut size = image.size_vec2();
size *= (ui.available_width() / size.x).min(1.0); size *= (ui.available_width() / size.x).min(1.0);
ui.image(texture, size); image.show_size(ui, size);
} else if let Some(colored_text) = colored_text { } else if let Some(colored_text) = colored_text {
colored_text.ui(ui); colored_text.ui(ui);
} else if let Some(text) = &text { } else if let Some(text) = &text {
@ -270,16 +269,3 @@ impl ColoredText {
} }
} }
} }
// ----------------------------------------------------------------------------
fn load_image(image_data: &[u8]) -> Result<egui::ColorImage, image::ImageError> {
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(),
))
}

6
egui_extras/CHANGELOG.md Normal file
View file

@ -0,0 +1,6 @@
# Changelog for egui_extras
All notable changes to the `egui_extras` integration will be noted in this file.
## Unreleased
* `RetainedImage`: conventience for loading svg, png, jpeg etc and showing them in egui.

48
egui_extras/Cargo.toml Normal file
View file

@ -0,0 +1,48 @@
[package]
name = "egui_extras"
version = "0.16.0"
authors = ["Emil Ernerfeldt <emil.ernerfeldt@gmail.com>"]
description = "Extra functionality and widgets for the egui GUI library"
edition = "2021"
rust-version = "1.56"
homepage = "https://github.com/emilk/egui"
license = "MIT OR Apache-2.0"
readme = "../README.md"
repository = "https://github.com/emilk/egui"
categories = ["gui", "game-development"]
keywords = ["gui", "imgui", "immediate", "portable", "gamedev"]
include = [
"../LICENSE-APACHE",
"../LICENSE-MIT",
"**/*.rs",
"Cargo.toml",
]
[package.metadata.docs.rs]
all-features = true
[lib]
[features]
default = []
# Support loading svg images
svg = ["resvg", "tiny-skia", "usvg"]
[dependencies]
egui = { version = "0.16.1", path = "../egui", default-features = false, features = ["single_threaded"] }
parking_lot = "0.11"
# Optional dependencies:
# Add support for loading images with the `image` crate.
# You also need to ALSO opt-in to the image formats you want to support, like so:
# image = { version = "0.24", features = ["jpeg", "png"] }
image = { version = "0.24", optional = true, default-features = false, features = [] }
# svg feature
resvg = { version = "0.22", optional = true }
tiny-skia = { version = "0.6", optional = true }
usvg = { version = "0.22", optional = true }

8
egui_extras/README.md Normal file
View file

@ -0,0 +1,8 @@
# egui_extras
[![Latest version](https://img.shields.io/crates/v/egui_extras.svg)](https://crates.io/crates/egui_extras)
[![Documentation](https://docs.rs/egui_extras/badge.svg)](https://docs.rs/egui_extras)
![MIT](https://img.shields.io/badge/license-MIT-blue.svg)
![Apache](https://img.shields.io/badge/license-Apache-blue.svg)
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`.

177
egui_extras/src/image.rs Normal file
View 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
View 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;

View file

@ -28,6 +28,7 @@ cargo doc --document-private-items --no-deps --all-features
(cd eframe && cargo check --no-default-features --features "egui_glow") (cd eframe && cargo check --no-default-features --features "egui_glow")
(cd epi && cargo check --no-default-features) (cd epi && cargo check --no-default-features)
(cd egui_demo_lib && cargo check --no-default-features) (cd egui_demo_lib && cargo check --no-default-features)
(cd egui_extras && cargo check --no-default-features)
# (cd egui_web && cargo check --no-default-features) # we need to pick webgl or glow backend # (cd egui_web && cargo check --no-default-features) # we need to pick webgl or glow backend
# (cd egui-winit && cargo check --no-default-features) # we don't pick singlethreaded or multithreaded # (cd egui-winit && cargo check --no-default-features) # we don't pick singlethreaded or multithreaded
(cd egui_glium && cargo check --no-default-features) (cd egui_glium && cargo check --no-default-features)
@ -36,6 +37,7 @@ cargo doc --document-private-items --no-deps --all-features
(cd eframe && cargo check --all-features) (cd eframe && cargo check --all-features)
(cd egui && cargo check --all-features) (cd egui && cargo check --all-features)
(cd egui_extras && cargo check --all-features)
(cd egui_glium && cargo check --all-features) (cd egui_glium && cargo check --all-features)
(cd egui_glow && cargo check --all-features) (cd egui_glow && cargo check --all-features)
(cd egui_web && cargo check --all-features) (cd egui_web && cargo check --all-features)

View file

@ -4,6 +4,6 @@ script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
cd "$script_path/.." cd "$script_path/.."
cargo doc -p egui_web --target wasm32-unknown-unknown --lib --no-deps --all-features cargo doc -p egui_web --target wasm32-unknown-unknown --lib --no-deps --all-features
cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui-winit -p egui_glium -p egui_glow --lib --no-deps --all-features --open cargo doc -p emath -p epaint -p egui -p eframe -p epi -p egui_web -p egui-winit -p egui_extras -p egui_glium -p egui_glow --lib --no-deps --all-features --open
# cargo watch -c -x 'doc -p emath -p epaint -p egui --lib --no-deps --all-features' # cargo watch -c -x 'doc -p emath -p epaint -p egui --lib --no-deps --all-features'