From c3fc8997d647aaee5006ccd3a56dff00884d3b8c Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 21 Feb 2022 15:26:26 +0100 Subject: [PATCH] =?UTF-8?q?Introduce=20egui=5Fextras=20with=20RetainedImag?= =?UTF-8?q?e=20for=20loading=20svg,png,jpeg,=E2=80=A6=20(#1282)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/rust.yml | 2 +- ARCHITECTURE.md | 5 +- Cargo.lock | 94 ++++++++++----- Cargo.toml | 1 + eframe/Cargo.toml | 7 +- eframe/examples/download_image.rs | 34 ++---- eframe/examples/image.rs | 39 ++++--- eframe/examples/svg.rs | 79 ++----------- egui/src/widget_text.rs | 7 ++ egui_demo_lib/Cargo.toml | 1 + egui_demo_lib/src/apps/http_app.rs | 30 ++--- egui_extras/CHANGELOG.md | 6 + egui_extras/Cargo.toml | 48 ++++++++ egui_extras/README.md | 8 ++ egui_extras/src/image.rs | 177 +++++++++++++++++++++++++++++ egui_extras/src/lib.rs | 87 ++++++++++++++ sh/check.sh | 2 + sh/docs.sh | 2 +- 18 files changed, 453 insertions(+), 176 deletions(-) create mode 100644 egui_extras/CHANGELOG.md create mode 100644 egui_extras/Cargo.toml create mode 100644 egui_extras/README.md create mode 100644 egui_extras/src/image.rs create mode 100644 egui_extras/src/lib.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index eb595662..09df2c58 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -132,7 +132,7 @@ jobs: toolchain: 1.56.0 override: true - 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: name: cargo doc web diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 994c5be2..5381d322 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -5,7 +5,7 @@ Also see [`CONTRIBUTING.md`](https://github.com/emilk/egui/blob/master/CONTRIBUT ## 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. 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). +### `egui_extras` +This adds additional features on top of `egui`. + ### `epi` Depends only on `egui`. Adds a thin application level wrapper around `egui` for hosting an `egui` app inside of `eframe`. diff --git a/Cargo.lock b/Cargo.lock index 1d0beef3..7cbeb430 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -994,6 +994,7 @@ version = "0.16.0" dependencies = [ "egui", "egui-winit", + "egui_extras", "egui_glium", "egui_glow", "egui_web", @@ -1001,10 +1002,7 @@ dependencies = [ "epi", "image", "poll-promise", - "resvg", "rfd", - "tiny-skia", - "usvg", ] [[package]] @@ -1053,6 +1051,7 @@ dependencies = [ "chrono", "criterion", "egui", + "egui_extras", "ehttp", "enum-map", "epi", @@ -1063,6 +1062,18 @@ dependencies = [ "unicode_names2", ] +[[package]] +name = "egui_extras" +version = "0.16.0" +dependencies = [ + "egui", + "image", + "parking_lot", + "resvg", + "tiny-skia", + "usvg", +] + [[package]] name = "egui_glium" version = "0.16.0" @@ -1335,14 +1346,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fontdb" -version = "0.7.0" +name = "fontconfig-parser" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b07f5c05414a0d8caba4c17eef8dc8b5c8955fc7c68d324191c7a56d3f3449" +checksum = "82cea2adebf32a9b104b8ffb308b5fb3b456f04cc76c294c3c85025c8a5d75f4" 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", "memmap2 0.5.2", - "ttf-parser 0.12.3", + "ttf-parser 0.15.0", ] [[package]] @@ -1515,6 +1536,16 @@ dependencies = [ "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]] name = "gimli" version = "0.26.1" @@ -1764,7 +1795,7 @@ dependencies = [ "bytemuck", "byteorder", "color_quant", - "jpeg-decoder 0.2.1", + "jpeg-decoder", "num-iter", "num-rational", "num-traits", @@ -1836,15 +1867,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jpeg-decoder" -version = "0.1.22" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "229d53d58899083193af11e15917b5640cd40b29ff475a1fe4ef725deb02d0f2" - -[[package]] -name = "jpeg-decoder" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbcf0244f6597be39ab8d9203f574cafb529ae8c698afa2182f7b3c3205a4a9c" +checksum = "105fb082d64e2100074587f59a74231f771750c664af903f1f9f76c9dedfc6f1" [[package]] name = "js-sys" @@ -2671,11 +2696,12 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "resvg" -version = "0.20.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d94a32ca845cdda27237a40beba9bd3d3858ac8fc5356eb9442bdeecfe34d9e0" +checksum = "2e702d1e8e00a3a0717b96244cba840f34f542d8f23097c8903266c4e2975658" dependencies = [ - "jpeg-decoder 0.1.22", + "gif", + "jpeg-decoder", "log", "pico-args", "png", @@ -2801,14 +2827,14 @@ dependencies = [ [[package]] name = "rustybuzz" -version = "0.4.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44561062e583c4873162861261f16fd1d85fe927c4904d71329a4fe43dc355ef" +checksum = "25ff94f20221325d000e552781713e53b0d85c1d9551b6f420d12daf5a08eace" dependencies = [ "bitflags", "bytemuck", "smallvec", - "ttf-parser 0.12.3", + "ttf-parser 0.15.0", "unicode-bidi-mirroring", "unicode-ccc", "unicode-general-category", @@ -3339,18 +3365,18 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "ttf-parser" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6" - [[package]] name = "ttf-parser" version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ccbe8381883510b6a2d8f1e32905bddd178c11caef8083086d0c0c9ab0ac281" +[[package]] +name = "ttf-parser" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c74c96594835e10fa545e2a51e8709f30b173a092bfd6036ef2cec53376244f3" + [[package]] name = "tts" version = "0.20.2" @@ -3472,9 +3498,9 @@ dependencies = [ [[package]] name = "usvg" -version = "0.20.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00f064d38f79ff69e3160e2fba884e4ede897061c15178041a3976371c68cab1" +checksum = "a261d60a7215fa339482047cc3dafd4e22e2bf34396aaebef2b707355bbb39c0" dependencies = [ "base64", "data-url", @@ -3490,7 +3516,7 @@ dependencies = [ "simplecss", "siphasher", "svgtypes", - "ttf-parser 0.12.3", + "ttf-parser 0.15.0", "unicode-bidi", "unicode-script", "unicode-vo", @@ -3733,6 +3759,12 @@ dependencies = [ "webpki", ] +[[package]] +name = "weezl" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b77fdfd5a253be4ab714e4ffa3c49caf146b4de743e97510c0656cf90f1e8e" + [[package]] name = "wepoll-ffi" version = "0.1.2" diff --git a/Cargo.toml b/Cargo.toml index 53b381e2..a8113833 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "egui_demo_app", "egui_demo_lib", + "egui_extras", "egui_glium", "egui_glow", "egui_web", diff --git a/eframe/Cargo.toml b/eframe/Cargo.toml index a9ff85c3..30778526 100644 --- a/eframe/Cargo.toml +++ b/eframe/Cargo.toml @@ -64,12 +64,9 @@ egui_web = { version = "0.16.0", path = "../egui_web", default-features = false, [dev-dependencies] +# For examples: +egui_extras = { path = "../egui_extras", features = ["image", "svg"] } ehttp = "0.2" image = { version = "0.24", default-features = false, features = ["jpeg", "png"] } poll-promise = "0.1" rfd = "0.7" - -# svg.rs example: -resvg = "0.20" -tiny-skia = "0.6" -usvg = "0.20" diff --git a/eframe/examples/download_image.rs b/eframe/examples/download_image.rs index e350ac56..dede08c4 100644 --- a/eframe/examples/download_image.rs +++ b/eframe/examples/download_image.rs @@ -1,6 +1,7 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release use eframe::{egui, epi}; +use egui_extras::RetainedImage; use poll_promise::Promise; fn main() { @@ -11,7 +12,7 @@ fn main() { #[derive(Default)] struct MyApp { /// `None` when download hasn't started yet. - promise: Option>>, + promise: Option>>, } impl epi::App for MyApp { @@ -24,14 +25,13 @@ impl epi::App for MyApp { // Begin download. // 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. - let ctx = ctx.clone(); let frame = frame.clone(); let (sender, promise) = Promise::new(); let request = ehttp::Request::get("https://picsum.photos/seed/1.759706314/1024"); 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 - let texture = response.and_then(|response| parse_response(&ctx, response)); - sender.send(texture); // send the results back to the UI thread. }); promise }); @@ -43,24 +43,17 @@ impl epi::App for MyApp { Some(Err(err)) => { ui.colored_label(egui::Color32::RED, err); // something went wrong } - Some(Ok(texture)) => { - let mut size = texture.size_vec2(); - size *= (ui.available_width() / size.x).min(1.0); - size *= (ui.available_height() / size.y).min(1.0); - ui.image(texture, size); + Some(Ok(image)) => { + image.show_max_size(ui, ui.available_size()); } }); } } -fn parse_response( - ctx: &egui::Context, - response: ehttp::Response, -) -> Result { +fn parse_response(response: ehttp::Response) -> Result { let content_type = response.content_type().unwrap_or_default(); if content_type.starts_with("image/") { - let image = load_image(&response.bytes).map_err(|err| err.to_string())?; - Ok(ctx.load_texture("my-image", image)) + RetainedImage::from_image_bytes(&response.url, &response.bytes) } else { Err(format!( "Expected image, found content-type {:?}", @@ -68,14 +61,3 @@ fn parse_response( )) } } - -fn load_image(image_data: &[u8]) -> Result { - 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(), - )) -} diff --git a/eframe/examples/image.rs b/eframe/examples/image.rs index a47e6c5b..97575c23 100644 --- a/eframe/examples/image.rs +++ b/eframe/examples/image.rs @@ -1,10 +1,22 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release use eframe::{egui, epi}; +use egui_extras::RetainedImage; -#[derive(Default)] struct MyApp { - texture: Option, + 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 { @@ -13,17 +25,15 @@ impl epi::App for MyApp { } 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| { 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.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(); eframe::run_native(Box::new(MyApp::default()), options); } - -fn load_image(image_data: &[u8]) -> Result { - 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(), - )) -} diff --git a/eframe/examples/svg.rs b/eframe/examples/svg.rs index e4aff0cf..b7d362f9 100644 --- a/eframe/examples/svg.rs +++ b/eframe/examples/svg.rs @@ -1,82 +1,23 @@ //! 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}; -/// Load an SVG and rasterize it into an egui image. -fn load_svg_data(svg_data: &[u8]) -> Result { - 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, -} - -impl SvgImage { - /// Pass itn the bytes of an SVG that you've loaded from disk - pub fn from_svg_data(bytes: &[u8]) -> Result { - 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 { - svg_image: SvgImage, + svg_image: egui_extras::RetainedImage, } impl Default for MyApp { fn default() -> 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(), } } } diff --git a/egui/src/widget_text.rs b/egui/src/widget_text.rs index 4e5659cd..c9fb64e2 100644 --- a/egui/src/widget_text.rs +++ b/egui/src/widget_text.rs @@ -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 for RichText { #[inline] fn from(text: String) -> Self { diff --git a/egui_demo_lib/Cargo.toml b/egui_demo_lib/Cargo.toml index 38447b14..689c7c47 100644 --- a/egui_demo_lib/Cargo.toml +++ b/egui_demo_lib/Cargo.toml @@ -40,6 +40,7 @@ syntax_highlighting = ["syntect"] [dependencies] 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" } chrono = { version = "0.4", features = ["js-sys", "wasmbind"], optional = true } diff --git a/egui_demo_lib/src/apps/http_app.rs b/egui_demo_lib/src/apps/http_app.rs index aa6ac4f4..c8215b42 100644 --- a/egui_demo_lib/src/apps/http_app.rs +++ b/egui_demo_lib/src/apps/http_app.rs @@ -1,3 +1,4 @@ +use egui_extras::RetainedImage; use poll_promise::Promise; struct Resource { @@ -7,7 +8,7 @@ struct Resource { text: Option, /// If set, the response was an image. - texture: Option, + image: Option, /// If set, the response was text with some supported syntax highlighting (e.g. ".rs" or ".md"). colored_text: Option, @@ -17,13 +18,11 @@ 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/") { - load_image(&response.bytes).ok() + RetainedImage::from_image_bytes(&response.url, &response.bytes).ok() } else { None }; - let texture = image.map(|image| ctx.load_texture(&response.url, image)); - let text = response.text(); let colored_text = text.and_then(|text| syntax_highlighting(ctx, &response, text)); let text = text.map(|text| text.to_owned()); @@ -31,7 +30,7 @@ impl Resource { Self { response, text, - texture, + image, colored_text, } } @@ -151,7 +150,7 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) { let Resource { response, text, - texture, + image, colored_text, } = resource; @@ -198,10 +197,10 @@ fn ui_resource(ui: &mut egui::Ui, resource: &Resource) { ui.separator(); } - if let Some(texture) = texture { - let mut size = texture.size_vec2(); + if let Some(image) = image { + let mut size = image.size_vec2(); 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 { colored_text.ui(ui); } else if let Some(text) = &text { @@ -270,16 +269,3 @@ impl ColoredText { } } } - -// ---------------------------------------------------------------------------- - -fn load_image(image_data: &[u8]) -> Result { - 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(), - )) -} diff --git a/egui_extras/CHANGELOG.md b/egui_extras/CHANGELOG.md new file mode 100644 index 00000000..d16a58db --- /dev/null +++ b/egui_extras/CHANGELOG.md @@ -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. diff --git a/egui_extras/Cargo.toml b/egui_extras/Cargo.toml new file mode 100644 index 00000000..7fed0fae --- /dev/null +++ b/egui_extras/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "egui_extras" +version = "0.16.0" +authors = ["Emil Ernerfeldt "] +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 } diff --git a/egui_extras/README.md b/egui_extras/README.md new file mode 100644 index 00000000..4909fd97 --- /dev/null +++ b/egui_extras/README.md @@ -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`. diff --git a/egui_extras/src/image.rs b/egui_extras/src/image.rs new file mode 100644 index 00000000..4015acd3 --- /dev/null +++ b/egui_extras/src/image.rs @@ -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, + /// Lazily loaded when we have an egui context. + texture: Mutex>, +} + +impl RetainedImage { + pub fn from_color_image(debug_name: impl Into, 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, + image_bytes: &[u8], + ) -> Result { + 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, svg_bytes: &[u8]) -> Result { + 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, svg_str: &str) -> Result { + 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 { + 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 { + 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) +} diff --git a/egui_extras/src/lib.rs b/egui_extras/src/lib.rs new file mode 100644 index 00000000..c51f54a8 --- /dev/null +++ b/egui_extras/src/lib.rs @@ -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; diff --git a/sh/check.sh b/sh/check.sh index 72fb1c64..e9d30930 100755 --- a/sh/check.sh +++ b/sh/check.sh @@ -28,6 +28,7 @@ cargo doc --document-private-items --no-deps --all-features (cd eframe && cargo check --no-default-features --features "egui_glow") (cd epi && 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-winit && cargo check --no-default-features) # we don't pick singlethreaded or multithreaded (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 egui && cargo check --all-features) +(cd egui_extras && cargo check --all-features) (cd egui_glium && cargo check --all-features) (cd egui_glow && cargo check --all-features) (cd egui_web && cargo check --all-features) diff --git a/sh/docs.sh b/sh/docs.sh index 1a38d228..9803d7f1 100755 --- a/sh/docs.sh +++ b/sh/docs.sh @@ -4,6 +4,6 @@ script_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P ) cd "$script_path/.." 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'