From b311aa343a9550cb6dbff211676daca178c68f9f Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Fri, 8 Sep 2023 11:22:13 +0200 Subject: [PATCH 1/3] use new `Image` caching API from egui --- Cargo.toml | 25 ++----- examples/hello_world.rs | 2 + src/fetch_data.rs | 29 -------- src/image_loading.rs | 51 ------------- src/lib.rs | 159 +++++++--------------------------------- 5 files changed, 37 insertions(+), 229 deletions(-) delete mode 100644 src/fetch_data.rs delete mode 100644 src/image_loading.rs diff --git a/Cargo.toml b/Cargo.toml index 21d1940..0207478 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,37 +13,26 @@ documentation = "https://docs.rs/egui_commonmark" include = ["src/*.rs", "LICENSE-MIT", "LICENSE-APACHE", "Cargo.toml"] [dependencies] -egui = { git = "https://github.com/emilk/egui", rev = "2c7c598" } -image = { version = "0.24", default-features = false, features = ["png"] } -parking_lot = "0.12" -poll-promise = "0.3" +egui = { git = "https://github.com/emilk/egui", rev = "8003e6b" } pulldown-cmark = { version = "0.9.3", default-features = false } - syntect = { version = "5.0.0", optional = true, default-features = false, features = [ "default-fancy", ] } -resvg = { version = "0.35.0", optional = true } -usvg = { version = "0.35.0", optional = true } - -url = { version = "2.4", optional = true } -ehttp = { version = "0.3.0", optional = true } - document-features = { version = "0.2", optional = true } [features] ## Syntax highlighting for code blocks syntax_highlighting = ["syntect"] -## Support loading svg images -svg = ["resvg", "usvg"] - -## Images with urls will be downloaded and displayed -fetch = ["ehttp", "url", "image/jpeg"] - [dev-dependencies] -eframe = { git = "https://github.com/emilk/egui", rev = "2c7c598" } +eframe = { git = "https://github.com/emilk/egui", rev = "8003e6b" } +egui_extras = { git = "https://github.com/emilk/egui", rev = "8003e6b", features = [ + "all-loaders", + "log", +] } +image = { version = "0.24", default-features = false, features = ["png"] } [package.metadata.docs.rs] # docs.rs build can fail with the fetch feature enabled diff --git a/examples/hello_world.rs b/examples/hello_world.rs index c2fc482..f7cac1b 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -64,6 +64,8 @@ fn main() { "Markdown viewer", eframe::NativeOptions::default(), Box::new(move |cc| { + egui_extras::loaders::install(&cc.egui_ctx); + cc.egui_ctx.set_visuals(if use_dark_theme { egui::Visuals::dark() } else { diff --git a/src/fetch_data.rs b/src/fetch_data.rs deleted file mode 100644 index 76bc4d4..0000000 --- a/src/fetch_data.rs +++ /dev/null @@ -1,29 +0,0 @@ -#[cfg(not(feature = "fetch"))] -pub fn get_image_data(uri: &str, on_done: impl 'static + Send + FnOnce(Result, String>)) { - get_image_data_from_file(uri, on_done) -} - -#[cfg(feature = "fetch")] -pub fn get_image_data(uri: &str, on_done: impl 'static + Send + FnOnce(Result, String>)) { - let url = url::Url::parse(uri); - if url.is_ok() { - let uri = uri.to_owned(); - ehttp::fetch(ehttp::Request::get(&uri), move |result| match result { - Ok(response) => { - on_done(Ok(response.bytes)); - } - Err(err) => { - on_done(Err(err)); - } - }); - } else { - get_image_data_from_file(uri, on_done) - } -} - -fn get_image_data_from_file( - path: &str, - on_done: impl 'static + Send + FnOnce(Result, String>), -) { - on_done(std::fs::read(path).map_err(|err| err.to_string())); -} diff --git a/src/image_loading.rs b/src/image_loading.rs deleted file mode 100644 index a24df9c..0000000 --- a/src/image_loading.rs +++ /dev/null @@ -1,51 +0,0 @@ -use egui::ColorImage; - -pub fn load_image(url: &str, data: &[u8]) -> Result { - if url.ends_with(".svg") { - try_render_svg(data) - } else { - try_load_image(data).map_err(|err| err.to_string()) - } -} - -fn try_load_image(data: &[u8]) -> image::ImageResult { - let image = image::load_from_memory(data)?; - let image_buffer = image.to_rgba8(); - let size = [image.width() as usize, image.height() as usize]; - let pixels = image_buffer.as_flat_samples(); - - Ok(ColorImage::from_rgba_unmultiplied(size, pixels.as_slice())) -} - -#[cfg(not(feature = "svg"))] -fn try_render_svg(_data: &[u8]) -> Result { - Err("SVG support not enabled".to_owned()) -} - -#[cfg(feature = "svg")] -fn try_render_svg(data: &[u8]) -> Result { - use resvg::tiny_skia; - use usvg::{TreeParsing, TreeTextToPath}; - - let tree = { - let options = usvg::Options::default(); - let mut fontdb = usvg::fontdb::Database::new(); - fontdb.load_system_fonts(); - - let mut tree = usvg::Tree::from_data(data, &options).map_err(|err| err.to_string())?; - tree.convert_text(&fontdb); - resvg::Tree::from_usvg(&tree) - }; - - let size = tree.size.to_int_size(); - - let (w, h) = (size.width(), size.height()); - let mut pixmap = tiny_skia::Pixmap::new(w, h) - .ok_or_else(|| format!("Failed to create {w}x{h} SVG image"))?; - tree.render(tiny_skia::Transform::default(), &mut pixmap.as_mut()); - - Ok(ColorImage::from_rgba_unmultiplied( - [pixmap.width() as usize, pixmap.height() as usize], - &pixmap.take(), - )) -} diff --git a/src/lib.rs b/src/lib.rs index bdcf9b3..4cc02bb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,16 +22,9 @@ #![cfg_attr(feature = "document-features", doc = "# Features")] #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] -mod fetch_data; -mod image_loading; +use std::collections::HashMap; -use std::sync::Arc; -use std::{collections::HashMap, task::Poll}; - -use egui::TextureHandle; use egui::{self, epaint, Id, NumExt, Pos2, RichText, Sense, TextStyle, Ui, Vec2}; -use parking_lot::Mutex; -use poll_promise::Promise; use pulldown_cmark::{CowStr, HeadingLevel, Options}; #[cfg(feature = "syntax_highlighting")] @@ -49,52 +42,8 @@ struct ScrollableCache { split_points: Vec<(usize, Pos2, Pos2)>, } -#[derive(Default)] -struct ImageHandleCache { - cache: HashMap>>, -} - -impl ImageHandleCache { - fn clear(&mut self) { - self.cache.clear(); - } - - fn load(&mut self, ctx: &egui::Context, url: String) -> Poll> { - let promise = self.cache.entry(url.clone()).or_insert_with(|| { - let ctx = ctx.clone(); - let (sender, promise) = Promise::new(); - fetch_data::get_image_data(&url.clone(), move |result| { - match result { - Ok(bytes) => { - sender.send(parse_image(&ctx, &url, &bytes)); - } - - Err(err) => { - sender.send(Err(err)); - } - }; - ctx.request_repaint(); - }); - promise - }); - - promise.poll().map(|r| r.clone()) - } - - fn loaded(&self) -> impl Iterator { - self.cache - .values() - .flat_map(|p| p.ready()) - .flat_map(|r| r.as_ref().ok()) - } -} - /// A cache used for storing content such as images. pub struct CommonMarkCache { - // Everything stored here must take into account that the cache is for multiple - // CommonMarkviewers with different source_ids. - images: Arc>, - #[cfg(feature = "syntax_highlighting")] ps: SyntaxSet, @@ -134,7 +83,6 @@ impl std::fmt::Debug for CommonMarkCache { impl Default for CommonMarkCache { fn default() -> Self { Self { - images: Default::default(), #[cfg(feature = "syntax_highlighting")] ps: SyntaxSet::load_defaults_newlines(), #[cfg(feature = "syntax_highlighting")] @@ -188,8 +136,8 @@ impl CommonMarkCache { } /// Refetch all images - pub fn reload_images(&mut self) { - self.images.lock().clear(); + pub fn reload_images(&mut self, ui: &Ui) { + ui.ctx().forget_all_images(); } /// Clear the cache for all scrollable elements @@ -253,17 +201,6 @@ impl CommonMarkCache { .unwrap_or_else(|| &self.ts.themes[default_theme(ui)]) } - fn max_image_width(&self, options: &CommonMarkOptions) -> f32 { - let mut max = 0.0; - for i in self.images.lock().loaded() { - let width = options.image_scaled(i)[0]; - if width >= max { - max = width; - } - } - max - } - fn scroll(&mut self, id: &Id) -> &mut ScrollableCache { if !self.scroll.contains_key(id) { self.scroll.insert(*id, Default::default()); @@ -304,25 +241,6 @@ impl Default for CommonMarkOptions { } impl CommonMarkOptions { - fn image_scaled(&self, texture: &TextureHandle) -> egui::Vec2 { - let size = texture.size(); - if let Some(max_width) = self.max_image_width { - let width = size[0]; - - if width > max_width { - let height = size[1] as f32; - let ratio = height / width as f32; - - let scaled_height = ratio * max_width as f32; - egui::vec2(max_width as f32, scaled_height) - } else { - egui::vec2(width as f32, size[1] as f32) - } - } else { - egui::vec2(size[0] as f32, size[1] as f32) - } - } - #[cfg(feature = "syntax_highlighting")] fn curr_theme(&self, ui: &Ui) -> &str { if ui.style().visuals.dark_mode { @@ -451,7 +369,6 @@ struct Link { } struct Image { - handle: Poll>, url: String, alt_text: Vec, } @@ -501,7 +418,7 @@ impl CommonMarkViewerInternal { text: &str, populate_split_points: bool, ) { - let max_width = self.max_width(cache, options, ui); + let max_width = self.max_width(options, ui); let layout = egui::Layout::left_to_right(egui::Align::BOTTOM).with_main_wrap(true); ui.allocate_ui_with_layout(egui::vec2(max_width, 0.0), layout, |ui| { @@ -575,7 +492,7 @@ impl CommonMarkViewerInternal { ui.set_height(page_size.y); let layout = egui::Layout::left_to_right(egui::Align::BOTTOM).with_main_wrap(true); - let max_width = self.max_width(cache, options, ui); + let max_width = self.max_width(options, ui); ui.allocate_ui_with_layout(egui::vec2(max_width, 0.0), layout, |ui| { ui.spacing_mut().item_spacing.x = 0.0; let scroll_cache = cache.scroll(&self.source_id); @@ -636,8 +553,8 @@ impl CommonMarkViewerInternal { self.table(events, cache, options, ui, max_width); } - fn max_width(&self, cache: &CommonMarkCache, options: &CommonMarkOptions, ui: &Ui) -> f32 { - let max_image_width = cache.max_image_width(options); + fn max_width(&self, options: &CommonMarkOptions, ui: &Ui) -> f32 { + let max_image_width = options.max_image_width.unwrap_or(0) as f32; let available_width = ui.available_width(); let max_width = max_image_width.max(available_width); @@ -765,7 +682,7 @@ impl CommonMarkViewerInternal { max_width: f32, ) { match event { - pulldown_cmark::Event::Start(tag) => self.start_tag(ui, tag, cache, options), + pulldown_cmark::Event::Start(tag) => self.start_tag(ui, tag, options), pulldown_cmark::Event::End(tag) => self.end_tag(ui, tag, cache, options, max_width), pulldown_cmark::Event::Text(text) => { self.event_text(text, ui); @@ -806,13 +723,7 @@ impl CommonMarkViewerInternal { } } - fn start_tag( - &mut self, - ui: &mut Ui, - tag: pulldown_cmark::Tag, - cache: &mut CommonMarkCache, - options: &CommonMarkOptions, - ) { + fn start_tag(&mut self, ui: &mut Ui, tag: pulldown_cmark::Tag, options: &CommonMarkOptions) { match tag { pulldown_cmark::Tag::Paragraph => { if self.should_insert_newline { @@ -872,7 +783,14 @@ impl CommonMarkViewerInternal { text: String::new(), }); } - pulldown_cmark::Tag::Image(_, url, _) => self.start_image(url.to_string(), ui, cache), + pulldown_cmark::Tag::Image(_, url, _) => { + let url = if url.starts_with("http://") { + url.to_string() + } else { + format!("file://{url}") + }; + self.start_image(url); + } } } @@ -969,11 +887,8 @@ impl CommonMarkViewerInternal { } } - fn start_image(&mut self, url: String, ui: &Ui, cache: &CommonMarkCache) { - let handle = cache.images.lock().load(ui.ctx(), url.clone()); - + fn start_image(&mut self, url: String) { self.image = Some(Image { - handle, url, alt_text: Vec::new(), }); @@ -981,30 +896,17 @@ impl CommonMarkViewerInternal { fn end_image(&mut self, ui: &mut Ui, options: &CommonMarkOptions) { if let Some(image) = self.image.take() { - let url = &image.url; - match image.handle { - Poll::Ready(Ok(texture)) => { - let size = options.image_scaled(&texture); - let response = ui.image(&texture, size); - - if !image.alt_text.is_empty() && options.show_alt_text_on_hover { - response.on_hover_ui_at_pointer(|ui| { - for alt in image.alt_text { - ui.label(alt); - } - }); + let response = ui.add( + egui::Image::from_uri(&image.url) + .fit_to_original_size(None) + .max_width(self.max_width(options, ui)), + ); + if !image.alt_text.is_empty() && options.show_alt_text_on_hover { + response.on_hover_ui_at_pointer(|ui| { + for alt in image.alt_text { + ui.label(alt); } - } - Poll::Ready(Err(err)) => { - ui.colored_label( - ui.visuals().error_fg_color, - format!("Error loading {url}: {err}"), - ); - } - Poll::Pending => { - ui.spinner(); - ui.label(format!("Loading {url}…")); - } + }); } if self.should_insert_newline { @@ -1219,11 +1121,6 @@ fn width_body_space(ui: &Ui) -> f32 { ui.fonts(|f| f.glyph_width(&id, ' ')) } -fn parse_image(ctx: &egui::Context, url: &str, data: &[u8]) -> Result { - let image = image_loading::load_image(url, data)?; - Ok(ctx.load_texture(url, image, egui::TextureOptions::LINEAR)) -} - #[cfg(feature = "syntax_highlighting")] fn default_theme(ui: &Ui) -> &str { if ui.style().visuals.dark_mode { From b348c8c682cf06f59df7f4197816efcabe9a2906 Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:20:56 +0200 Subject: [PATCH 2/3] update to latest rev --- Cargo.toml | 7 ++++--- examples/hello_world.rs | 4 ++-- examples/link_hooks.rs | 8 ++++---- examples/scroll.rs | 2 +- src/lib.rs | 7 ++++--- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0207478..86bde9b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,8 +13,9 @@ documentation = "https://docs.rs/egui_commonmark" include = ["src/*.rs", "LICENSE-MIT", "LICENSE-APACHE", "Cargo.toml"] [dependencies] -egui = { git = "https://github.com/emilk/egui", rev = "8003e6b" } +egui = { git = "https://github.com/emilk/egui", rev = "ab48e60" } pulldown-cmark = { version = "0.9.3", default-features = false } +egui_extras = { git = "https://github.com/emilk/egui", rev = "ab48e60" } syntect = { version = "5.0.0", optional = true, default-features = false, features = [ "default-fancy", @@ -27,11 +28,11 @@ document-features = { version = "0.2", optional = true } syntax_highlighting = ["syntect"] [dev-dependencies] -eframe = { git = "https://github.com/emilk/egui", rev = "8003e6b" } -egui_extras = { git = "https://github.com/emilk/egui", rev = "8003e6b", features = [ +egui_extras = { git = "https://github.com/emilk/egui", rev = "ab48e60", features = [ "all-loaders", "log", ] } +eframe = { git = "https://github.com/emilk/egui", rev = "ab48e60" } image = { version = "0.24", default-features = false, features = ["png"] } [package.metadata.docs.rs] diff --git a/examples/hello_world.rs b/examples/hello_world.rs index f7cac1b..8128232 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -64,7 +64,7 @@ fn main() { "Markdown viewer", eframe::NativeOptions::default(), Box::new(move |cc| { - egui_extras::loaders::install(&cc.egui_ctx); + // egui_extras::loaders::install(&cc.egui_ctx); cc.egui_ctx.set_visuals(if use_dark_theme { egui::Visuals::dark() @@ -73,7 +73,7 @@ fn main() { }); Box::new(App { - cache: CommonMarkCache::default(), + cache: CommonMarkCache::new(&cc.egui_ctx), }) }), ) diff --git a/examples/link_hooks.rs b/examples/link_hooks.rs index b9b04ea..b0f6331 100644 --- a/examples/link_hooks.rs +++ b/examples/link_hooks.rs @@ -29,13 +29,13 @@ Check out the [previous](#prev) page."#; } fn main() { - let mut cache = CommonMarkCache::default(); - cache.add_link_hook("#next"); - cache.add_link_hook("#prev"); eframe::run_native( "Markdown link hooks", eframe::NativeOptions::default(), - Box::new(|_| { + Box::new(|cc| { + let mut cache = CommonMarkCache::new(&cc.egui_ctx); + cache.add_link_hook("#next"); + cache.add_link_hook("#prev"); Box::new(App { cache, curr_page: 0, diff --git a/examples/scroll.rs b/examples/scroll.rs index bd84873..9520b7d 100644 --- a/examples/scroll.rs +++ b/examples/scroll.rs @@ -65,7 +65,7 @@ fn main() { }); Box::new(App { - cache: CommonMarkCache::default(), + cache: CommonMarkCache::new(&cc.egui_ctx), }) }), ) diff --git a/src/lib.rs b/src/lib.rs index 4cc02bb..dc0bfaf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -79,9 +79,10 @@ impl std::fmt::Debug for CommonMarkCache { } } -#[allow(clippy::derivable_impls)] -impl Default for CommonMarkCache { - fn default() -> Self { +impl CommonMarkCache { + pub fn new(ctx: &egui::Context) -> Self { + egui_extras::loaders::install(ctx); + Self { #[cfg(feature = "syntax_highlighting")] ps: SyntaxSet::load_defaults_newlines(), From dfc9f23167d5fae03c1b668f4a72d169f5e8ec4f Mon Sep 17 00:00:00 2001 From: jprochazk <1665677+jprochazk@users.noreply.github.com> Date: Mon, 11 Sep 2023 16:23:37 +0200 Subject: [PATCH 3/3] handle `https` --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index dc0bfaf..109c55c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -785,7 +785,7 @@ impl CommonMarkViewerInternal { }); } pulldown_cmark::Tag::Image(_, url, _) => { - let url = if url.starts_with("http://") { + let url = if url.starts_with("http://") || url.starts_with("https://") { url.to_string() } else { format!("file://{url}")