diff --git a/src/app/mod.rs b/src/app/mod.rs index 4fdd3cf..5d6e03e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2915,6 +2915,33 @@ fn avatar_badge<'a>(name: &str, key: &str, size: f32) -> Element<'a, AppMessage> .into() } +thread_local! { + /// Cache of avatar image handles, keyed by a hash of the PNG bytes, so the + /// SAME `image::Handle` (and thus the same GPU texture id) is reused across + /// redraws. `image::Handle::from_bytes` mints a fresh *unique* id on every + /// call, so building handles inline in `view()` made iced re-upload the + /// texture on every repaint — including the redraws fired on each mouse move — + /// which showed up as constant flicker. Lives on the (single) UI thread. + static AVATAR_HANDLE_CACHE: std::cell::RefCell> = + std::cell::RefCell::new(HashMap::new()); +} + +/// A stable image handle for these exact PNG bytes (cached by content hash), so +/// it keeps the same id across redraws — see [`AVATAR_HANDLE_CACHE`]. +fn cached_image_handle(bytes: bytes::Bytes) -> iced::widget::image::Handle { + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + bytes.as_ref().hash(&mut hasher); + let key = hasher.finish(); + AVATAR_HANDLE_CACHE.with(|cache| { + cache + .borrow_mut() + .entry(key) + .or_insert_with(|| iced::widget::image::Handle::from_bytes(bytes)) + .clone() + }) +} + /// Render a participant's avatar (W4): the chosen preset image if any, else the /// monogram fallback. `name`/`key` feed the monogram; `size` is the diameter. fn avatar_view<'a>( @@ -2928,7 +2955,7 @@ fn avatar_view<'a>( .map(bytes::Bytes::from_static) .or_else(|| avatar.custom_png().map(bytes::Bytes::from)); match png_bytes { - Some(b) => iced::widget::image(iced::widget::image::Handle::from_bytes(b)) + Some(b) => iced::widget::image(cached_image_handle(b)) .width(iced::Length::Fixed(size)) .height(iced::Length::Fixed(size)) .into(),