From 62a7933de096a739da3a08be9d9408ab7e2abe24 Mon Sep 17 00:00:00 2001 From: kieran Date: Tue, 14 Jan 2025 11:17:37 +0000 Subject: [PATCH] feat: resize images --- Cargo.lock | 35 ++++++++++--- Cargo.toml | 4 +- src/app.rs | 8 +++ src/bin/zap_stream_app.rs | 7 +-- src/resources/logo.svg | 13 +---- src/route/home.rs | 2 +- src/route/mod.rs | 90 +++++++++++++++++-------------- src/route/profile.rs | 12 +++-- src/route/stream.rs | 99 +++++++++++++++++++---------------- src/services/ffmpeg_loader.rs | 24 ++++++--- src/stream_info.rs | 3 ++ src/widgets/avatar.rs | 14 ++--- src/widgets/chat.rs | 2 +- src/widgets/chat_message.rs | 15 ++++-- src/widgets/stream_tile.rs | 45 +++++++--------- 15 files changed, 212 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 663b2c5..ddbfb05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1362,11 +1362,11 @@ dependencies = [ [[package]] name = "directories" -version = "5.0.1" +version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", ] [[package]] @@ -1375,7 +1375,7 @@ version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys", + "dirs-sys 0.4.1", ] [[package]] @@ -1386,10 +1386,22 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.4.6", "windows-sys 0.48.0", ] +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.0", + "windows-sys 0.59.0", +] + [[package]] name = "dispatch" version = "0.2.0" @@ -1512,7 +1524,7 @@ dependencies = [ [[package]] name = "egui-video" version = "0.8.0" -source = "git+https://github.com/v0l/egui-video.git?rev=d2ea3b4db21eb870a207db19e4cd21c7d1d24836#d2ea3b4db21eb870a207db19e4cd21c7d1d24836" +source = "git+https://github.com/v0l/egui-video.git?rev=11db7d0c30070529a36bfb050844cdb75c32902b#11db7d0c30070529a36bfb050844cdb75c32902b" dependencies = [ "anyhow", "atomic", @@ -4517,6 +4529,17 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "redox_users" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" +dependencies = [ + "getrandom", + "libredox", + "thiserror 2.0.9", +] + [[package]] name = "regex" version = "1.11.0" diff --git a/Cargo.toml b/Cargo.toml index 7f89de3..9f48582 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,8 @@ bech32 = "0.11.0" anyhow = "^1.0.91" itertools = "0.14.0" serde = { version = "1.0.214", features = ["derive"] } -directories = "5.0.1" -egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "d2ea3b4db21eb870a207db19e4cd21c7d1d24836" } +directories = "6.0.0" +egui-video = { git = "https://github.com/v0l/egui-video.git", rev = "11db7d0c30070529a36bfb050844cdb75c32902b" } egui_qr = { git = "https://git.v0l.io/Kieran/egui_qr.git", rev = "f9cf52b7eae353fa9e59ed0358151211d48824d1" } # notedeck stuff diff --git a/src/app.rs b/src/app.rs index e47672f..cebe944 100644 --- a/src/app.rs +++ b/src/app.rs @@ -62,6 +62,12 @@ impl ZapStreamApp { .insert(FontFamily::Proportional, vec!["Outfit".to_string()]); cc.egui_ctx.set_fonts(fd); + // ffmpeg log redirect + unsafe { + egui_video::ffmpeg_sys_the_third::av_log_set_callback(Some( + egui_video::ffmpeg_rs_raw::av_log_redirect, + )); + } let (tx, rx) = mpsc::channel(); Self { current: RouteType::HomePage, @@ -103,6 +109,8 @@ impl notedeck::App for ZapStreamApp { }, ); + //ui.ctx().set_debug_on_hover(true); + let app_frame = egui::containers::Frame::default().outer_margin(self.frame_margin()); // handle app state changes diff --git a/src/bin/zap_stream_app.rs b/src/bin/zap_stream_app.rs index 10bf746..417d8fe 100644 --- a/src/bin/zap_stream_app.rs +++ b/src/bin/zap_stream_app.rs @@ -2,7 +2,7 @@ use anyhow::Result; use directories::ProjectDirs; use eframe::Renderer; use egui::{Vec2, ViewportBuilder}; -use log::error; +use log::{error, info}; use zap_stream_app::app::ZapStreamApp; #[tokio::main] @@ -13,11 +13,12 @@ async fn main() -> Result<()> { options.viewport = ViewportBuilder::default().with_inner_size(Vec2::new(1300., 900.)); options.renderer = Renderer::Glow; - let data_path = ProjectDirs::from("stream", "zap", "app") + let data_path = ProjectDirs::from("stream", "zap", "zap_stream_app") .unwrap() - .config_dir() + .data_dir() .to_path_buf(); + info!("Data path: {}", data_path.display()); if let Err(e) = eframe::run_native( "zap.stream", options, diff --git a/src/resources/logo.svg b/src/resources/logo.svg index e11504f..3f9a9b1 100644 --- a/src/resources/logo.svg +++ b/src/resources/logo.svg @@ -1,13 +1,4 @@ - - - - - - - - - - - + diff --git a/src/route/home.rs b/src/route/home.rs index 6e7ddf9..c94c0b8 100644 --- a/src/route/home.rs +++ b/src/route/home.rs @@ -23,7 +23,7 @@ impl HomePage { } fn get_filters() -> Vec { - vec![Filter::new().kinds([30_311]).limit(100).build()] + vec![Filter::new().kinds([30_311, 30_313]).limit(100).build()] } } diff --git a/src/route/mod.rs b/src/route/mod.rs index 22828d4..e51b79b 100644 --- a/src/route/mod.rs +++ b/src/route/mod.rs @@ -3,7 +3,8 @@ use crate::services::ffmpeg_loader::FfmpegLoader; use crate::widgets::PlaceholderRect; use anyhow::{anyhow, bail}; use egui::load::SizedTexture; -use egui::{Context, Id, Image, ImageSource, TextureHandle, Ui}; +use egui::{Context, Id, Image, ImageSource, TextureHandle, Ui, Vec2}; +use egui_video::ffmpeg_rs_raw::Transcoder; use ehttp::Response; use enostr::EventClientMessage; use lnurl::lightning_address::LightningAddress; @@ -187,18 +188,31 @@ impl<'a, 'ctx> RouteServices<'a, 'ctx> { } } -const BLACK_PIXEL: [u8; 4] = [0, 0, 0, 0]; +const LOGO_BYTES: &[u8] = include_bytes!("../resources/logo.svg"); -pub fn image_from_cache<'a>(img_cache: &mut ImageCache, ui: &Ui, url: &str) -> Image<'a> { - if let Some(promise) = img_cache.map().get(url) { +pub fn image_from_cache<'a>( + img_cache: &mut ImageCache, + ui: &Ui, + url: &str, + size: Option, +) -> Image<'a> { + let cache_key = if let Some(s) = size { + format!("{}:{}", url, s) + } else { + url.to_string() + }; + if url.len() == 0 { + return Image::from_bytes(cache_key, LOGO_BYTES); + } + if let Some(promise) = img_cache.map().get(&cache_key) { match promise.poll() { Poll::Ready(Ok(t)) => Image::new(SizedTexture::from_handle(t)), - _ => Image::from_bytes(url.to_string(), &BLACK_PIXEL), + _ => Image::from_bytes(url.to_string(), LOGO_BYTES), } } else { - let fetch = fetch_img(img_cache, ui.ctx(), url); - img_cache.map_mut().insert(url.to_string(), fetch); - Image::from_bytes(url.to_string(), &BLACK_PIXEL) + let fetch = fetch_img(img_cache, ui.ctx(), url, size); + img_cache.map_mut().insert(cache_key.clone(), fetch); + Image::from_bytes(cache_key, LOGO_BYTES) } } @@ -206,52 +220,48 @@ fn fetch_img( img_cache: &ImageCache, ctx: &Context, url: &str, + size: Option, ) -> Promise> { - let k = ImageCache::key(url); - let dst_path = img_cache.cache_dir.join(k); + let name = ImageCache::key(url); + let dst_path = img_cache.cache_dir.join(&name); if dst_path.exists() { let ctx = ctx.clone(); - let url = url.to_owned(); - let dst_path = dst_path.clone(); - Promise::spawn_blocking(move || { + Promise::spawn_thread("load_from_disk", move || { info!("Loading image from disk: {}", dst_path.display()); - match FfmpegLoader::new().load_image(dst_path) { - Ok(img) => Ok(ctx.load_texture(&url, img, Default::default())), + match FfmpegLoader::new().load_image(dst_path, size) { + Ok(img) => Ok(ctx.load_texture(&name, img, Default::default())), Err(e) => Err(notedeck::Error::Generic(e.to_string())), } }) } else { - fetch_img_from_net(&dst_path, ctx, url) + let url = url.to_string(); + let ctx = ctx.clone(); + Promise::spawn_thread("load_from_net", move || { + let img = match fetch_img_from_net(&url).block_and_take() { + Ok(img) => img, + Err(e) => return Err(notedeck::Error::Generic(e.to_string())), + }; + std::fs::create_dir_all(&dst_path.parent().unwrap()).unwrap(); + std::fs::write(&dst_path, &img.bytes).unwrap(); + + info!("Loading image from net: {}", &url); + match FfmpegLoader::new().load_image(dst_path, size) { + Ok(img) => { + ctx.request_repaint(); + Ok(ctx.load_texture(&name, img, Default::default())) + } + Err(e) => Err(notedeck::Error::Generic(e.to_string())), + } + }) } } -fn fetch_img_from_net( - cache_path: &Path, - ctx: &Context, - url: &str, -) -> Promise> { +fn fetch_img_from_net(url: &str) -> Promise> { let (sender, promise) = Promise::new(); let request = ehttp::Request::get(url); - let ctx = ctx.clone(); - let cloned_url = url.to_owned(); - let cache_path = cache_path.to_owned(); + info!("Downloaded image: {}", url); ehttp::fetch(request, move |response| { - let handle = response - .and_then(|img| { - std::fs::create_dir_all(cache_path.parent().unwrap()).unwrap(); - std::fs::write(&cache_path, &img.bytes).unwrap(); - info!("Loading image from net: {}", cloned_url); - let img_loaded = FfmpegLoader::new() - .load_image(cache_path) - .map_err(|e| e.to_string())?; - - Ok(ctx.load_texture(&cloned_url, img_loaded, Default::default())) - }) - .map_err(notedeck::Error::Generic); - - sender.send(handle); - ctx.request_repaint(); + sender.send(response); }); - promise } diff --git a/src/route/profile.rs b/src/route/profile.rs index 0cedcd0..19a1dfa 100644 --- a/src/route/profile.rs +++ b/src/route/profile.rs @@ -35,10 +35,14 @@ impl NostrWidget for ProfilePage { ui.spacing_mut().item_spacing.y = 8.0; if let Some(banner) = profile.map(|p| p.banner()).flatten() { - image_from_cache(&mut services.ctx.img_cache, ui, banner) - .fit_to_exact_size(vec2(ui.available_width(), 360.0)) - .rounding(ROUNDING_DEFAULT) - .ui(ui); + image_from_cache( + &mut services.ctx.img_cache, + ui, + banner, + Some(vec2(ui.available_width(), 360.0)), + ) + .rounding(ROUNDING_DEFAULT) + .ui(ui); } else { ui.add(PlaceholderRect); } diff --git a/src/route/stream.rs b/src/route/stream.rs index 3e406fb..d174e38 100644 --- a/src/route/stream.rs +++ b/src/route/stream.rs @@ -1,14 +1,14 @@ use crate::link::NostrLink; use crate::route::RouteServices; -use crate::stream_info::StreamInfo; use crate::theme::{MARGIN_DEFAULT, NEUTRAL_800, ROUNDING_DEFAULT}; use crate::widgets::{ sub_or_poll, Chat, NostrWidget, PlaceholderRect, StreamPlayer, StreamTitle, WriteChat, }; -use egui::{vec2, Align, Frame, Layout, Response, Stroke, Ui, Vec2, Widget}; +use egui::{vec2, Align, Frame, Layout, Response, ScrollArea, Stroke, Ui, Vec2, Widget}; use nostrdb::{Filter, Note}; use crate::note_ref::NoteRef; +use crate::stream_info::StreamInfo; use crate::sub::SubRef; use std::borrow::Borrow; use std::collections::HashSet; @@ -95,49 +95,51 @@ impl StreamPage { let video_width = ui.available_width() - chat_w; let video_height = max_h.min((video_width / 16.0) * 9.0); - ui.with_layout( - Layout::left_to_right(Align::TOP).with_main_justify(true), - |ui| { - ui.vertical(|ui| { - ui.allocate_ui(vec2(video_width, video_height), |ui| { + ui.horizontal(|ui| { + ui.allocate_ui_with_layout( + vec2(video_width, max_h), + Layout::top_down_justified(Align::Min), + |ui| { + ScrollArea::vertical().show(ui, |ui| { if let Some(player) = &mut self.player { - player.ui(ui) + ui.add_sized(vec2(video_width, video_height), player); } else { - ui.add(PlaceholderRect) + ui.add_sized(vec2(video_width, video_height), PlaceholderRect); } + + ui.add_space(10.); + StreamTitle::new(event).render(ui, services); }); - ui.add_space(10.); - StreamTitle::new(event).render(ui, services); - }); - ui.allocate_ui_with_layout( - vec2(chat_w, max_h), - Layout::top_down_justified(Align::Min), - |ui| { - Frame::none() - .stroke(Stroke::new(1.0, NEUTRAL_800)) - .outer_margin(MARGIN_DEFAULT) - .rounding(ROUNDING_DEFAULT) - .show(ui, |ui| { - let chat_h = 60.0; - if let Some(c) = self.chat.as_mut() { - ui.allocate_ui( - vec2(ui.available_width(), ui.available_height() - chat_h), - |ui| { - c.render(ui, services); - }, - ); - } else { - ui.label("Loading.."); - } - if ui.available_height().is_finite() { - ui.add_space(ui.available_height() - chat_h); - } - self.new_msg.render(ui, services); - }); - }, - ); - }, - ); + }, + ); + ui.allocate_ui_with_layout( + vec2(chat_w, max_h), + Layout::top_down_justified(Align::Min), + |ui| { + Frame::none() + .stroke(Stroke::new(1.0, NEUTRAL_800)) + .outer_margin(MARGIN_DEFAULT) + .rounding(ROUNDING_DEFAULT) + .show(ui, |ui| { + let chat_h = 60.0; + if let Some(c) = self.chat.as_mut() { + ui.allocate_ui( + vec2(ui.available_width(), ui.available_height() - chat_h), + |ui| { + c.render(ui, services); + }, + ); + } else { + ui.label("Loading.."); + } + if ui.available_height().is_finite() { + ui.add_space(ui.available_height() - chat_h); + } + self.new_msg.render(ui, services); + }); + }, + ); + }); ui.response() } @@ -152,11 +154,16 @@ impl NostrWidget for StreamPage { .collect(); if let Some(event) = events.first() { - if let Some(stream) = event.streaming() { - if self.player.is_none() { - let p = StreamPlayer::new(ui.ctx(), &stream.to_string()); - self.player = Some(p); - } + if self.player.is_none() { + match event.kind() { + 30_311 => { + if let Some(u) = event.streaming().or(event.recording()) { + let p = StreamPlayer::new(ui.ctx(), &u.to_string()); + self.player = Some(p); + } + } + _ => {} + }; } if self.chat.is_none() { diff --git a/src/services/ffmpeg_loader.rs b/src/services/ffmpeg_loader.rs index 2e49384..54e51b6 100644 --- a/src/services/ffmpeg_loader.rs +++ b/src/services/ffmpeg_loader.rs @@ -1,5 +1,5 @@ use anyhow::Error; -use egui::ColorImage; +use egui::{ColorImage, Vec2}; use egui_video::ffmpeg_rs_raw::{get_frame_from_hw, Decoder, Demuxer, Scaler}; use egui_video::ffmpeg_sys_the_third::{av_frame_free, av_packet_free, AVPixelFormat}; use egui_video::media_player::video_frame_to_image; @@ -12,17 +12,25 @@ impl FfmpegLoader { Self {} } - pub fn load_image(&self, path: PathBuf) -> Result { + pub fn load_image(&self, path: PathBuf, size: Option) -> Result { let demux = Demuxer::new(path.to_str().unwrap())?; - Self::load_image_from_demuxer(demux) + Self::load_image_from_demuxer(demux, size) } - pub fn load_image_bytes(&self, key: &str, data: &'static [u8]) -> Result { + pub fn load_image_bytes( + &self, + key: &str, + data: &'static [u8], + size: Option, + ) -> Result { let demux = Demuxer::new_custom_io(data, Some(key.to_string()))?; - Self::load_image_from_demuxer(demux) + Self::load_image_from_demuxer(demux, size) } - fn load_image_from_demuxer(mut demuxer: Demuxer) -> Result { + fn load_image_from_demuxer( + mut demuxer: Demuxer, + size: Option, + ) -> Result { unsafe { let info = demuxer.probe_input()?; @@ -46,8 +54,8 @@ impl FfmpegLoader { let mut frame = get_frame_from_hw(*frame)?; let frame_rgb = scaler.process_frame( frame, - (*frame).width as u16, - (*frame).height as u16, + size.map(|s| s.x as u16).unwrap_or((*frame).width as u16), + size.map(|s| s.y as u16).unwrap_or((*frame).height as u16), rgb, )?; av_frame_free(&mut frame); diff --git a/src/stream_info.rs b/src/stream_info.rs index afd8d80..f6a966e 100644 --- a/src/stream_info.rs +++ b/src/stream_info.rs @@ -89,6 +89,9 @@ impl StreamInfo for Note<'_> { /// Is the stream playable by this app fn can_play(&self) -> bool { + if self.kind() == 30_313 { + return true; // n94-stream can always be played + } if let Some(stream) = self.streaming() { stream.contains(".m3u8") } else { diff --git a/src/widgets/avatar.rs b/src/widgets/avatar.rs index f22616c..6b5cd4c 100644 --- a/src/widgets/avatar.rs +++ b/src/widgets/avatar.rs @@ -1,5 +1,6 @@ use crate::route::image_from_cache; -use egui::{vec2, Color32, Pos2, Response, Rounding, Sense, Ui, Vec2, Widget}; +use crate::theme::NEUTRAL_800; +use egui::{vec2, Response, Rounding, Sense, Ui, Vec2, Widget}; use nostrdb::{Ndb, NdbProfile, Transaction}; use notedeck::ImageCache; @@ -42,11 +43,8 @@ impl Avatar { fn placeholder(ui: &mut Ui, size: f32) -> Response { let (response, painter) = ui.allocate_painter(vec2(size, size), Sense::click()); - painter.circle_filled( - Pos2::new(size / 2., size / 2.), - size / 2., - Color32::from_rgb(200, 200, 200), - ); + let pos = response.rect.min + vec2(size / 2., size / 2.); + painter.circle_filled(pos, size / 2., NEUTRAL_800); response } @@ -57,9 +55,7 @@ impl Avatar { return Self::placeholder(ui, size_v); } match &self.image { - Some(img) => image_from_cache(img_cache, ui, img) - .max_size(size) - .fit_to_exact_size(size) + Some(img) => image_from_cache(img_cache, ui, img, Some(size)) .rounding(Rounding::same(size_v)) .sense(Sense::click()) .ui(ui), diff --git a/src/widgets/chat.rs b/src/widgets/chat.rs index 7eda016..f2d13d8 100644 --- a/src/widgets/chat.rs +++ b/src/widgets/chat.rs @@ -64,7 +64,7 @@ impl NostrWidget for Chat { 1311 => { let profile = services.profile(ev.pubkey()); ChatMessage::new(&stream, &ev, &profile) - .render(ui, services.ctx.img_cache); + .render(ui, services); } 9735 => { if let Ok(zap) = Zap::from_receipt(ev) { diff --git a/src/widgets/chat_message.rs b/src/widgets/chat_message.rs index 3b7bb60..13c5ce3 100644 --- a/src/widgets/chat_message.rs +++ b/src/widgets/chat_message.rs @@ -1,3 +1,5 @@ +use crate::link::NostrLink; +use crate::route::{RouteServices, RouteType}; use crate::stream_info::StreamInfo; use crate::theme::{NEUTRAL_500, PRIMARY}; use crate::widgets::Avatar; @@ -5,7 +7,6 @@ use eframe::epaint::text::TextWrapMode; use egui::text::LayoutJob; use egui::{Align, Color32, Label, Response, TextFormat, Ui}; use nostrdb::{NdbProfile, Note}; -use notedeck::ImageCache; pub struct ChatMessage<'a> { stream: &'a Note<'a>, @@ -26,7 +27,7 @@ impl<'a> ChatMessage<'a> { } } - pub fn render(self, ui: &mut Ui, img_cache: &mut ImageCache) -> Response { + pub fn render(self, ui: &mut Ui, services: &mut RouteServices) -> Response { ui.horizontal_wrapped(|ui| { let mut job = LayoutJob::default(); // TODO: avoid this somehow @@ -48,9 +49,15 @@ impl<'a> ChatMessage<'a> { format.color = Color32::WHITE; job.append(self.ev.content(), 5.0, format.clone()); - Avatar::from_profile(self.profile) + if Avatar::from_profile(self.profile) .size(24.) - .render(ui, img_cache); + .render(ui, services.ctx.img_cache) + .clicked() + { + services.navigate(RouteType::ProfilePage { + link: NostrLink::profile(self.ev.pubkey()), + }) + } ui.add(Label::new(job).wrap_mode(TextWrapMode::Wrap)); // consume reset of space diff --git a/src/widgets/stream_tile.rs b/src/widgets/stream_tile.rs index b7c6658..c65dc94 100644 --- a/src/widgets/stream_tile.rs +++ b/src/widgets/stream_tile.rs @@ -5,10 +5,9 @@ use crate::theme::{NEUTRAL_800, NEUTRAL_900, PRIMARY, ROUNDING_DEFAULT}; use crate::widgets::avatar::Avatar; use eframe::epaint::{Rounding, Vec2}; use egui::epaint::RectShape; -use egui::load::TexturePoll; use egui::{ - vec2, Color32, CursorIcon, FontId, Label, Pos2, Rect, Response, RichText, Sense, TextWrapMode, - Ui, + vec2, Color32, CursorIcon, FontId, ImageSource, Label, Pos2, Rect, Response, RichText, Sense, + TextWrapMode, Ui, }; use nostrdb::Note; @@ -34,33 +33,27 @@ impl<'a> StreamEvent<'a> { let (response, painter) = ui.allocate_painter(Vec2::new(w, h), Sense::click()); let cover = if ui.is_rect_visible(response.rect) { - self.event - .image() - .map(|p| image_from_cache(services.ctx.img_cache, ui, p)) + self.event.image().map(|p| { + image_from_cache(services.ctx.img_cache, ui, p, Some(Vec2::new(w, h))) + .rounding(ROUNDING_DEFAULT) + }) } else { None }; - if let Some(cover) = cover.map(|c| { - c.rounding(Rounding::same(12.)) - .load_for_size(painter.ctx(), Vec2::new(w, h)) - }) { - match cover { - Ok(TexturePoll::Ready { texture }) => { - painter.add(RectShape { - rect: response.rect, - rounding: Rounding::same(ROUNDING_DEFAULT), - fill: Color32::WHITE, - stroke: Default::default(), - blur_width: 0.0, - fill_texture_id: texture.id, - uv: Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)), - }); - } - _ => { - painter.rect_filled(response.rect, ROUNDING_DEFAULT, NEUTRAL_800); - } - } + if let Some(cover) = cover { + painter.add(RectShape { + rect: response.rect, + rounding: Rounding::same(ROUNDING_DEFAULT), + fill: Color32::WHITE, + stroke: Default::default(), + blur_width: 0.0, + fill_texture_id: match cover.source(ui.ctx()) { + ImageSource::Texture(t) => t.id, + _ => Default::default(), + }, + uv: Rect::from_min_max(Pos2::new(0.0, 0.0), Pos2::new(1.0, 1.0)), + }); } else { painter.rect_filled(response.rect, ROUNDING_DEFAULT, NEUTRAL_800); }