notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 45bbed29397ba53e595a025c8568faa2ec5bf00f
parent 9c2cf3af751af6901f357256d30440c14b78f16a
Author: kernelkind <kernelkind@gmail.com>
Date:   Sat, 31 Jan 2026 18:51:24 -0500

perf-fix(banner): use our custom img loader via jobs for banner

instead of egui_extras. Also, give a size hint so we don't load
massive banners in egui

Closes: https://github.com/damus-io/notedeck/issues/918

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck_columns/src/ui/profile/edit.rs | 2+-
Mcrates/notedeck_columns/src/ui/profile/mod.rs | 2++
Mcrates/notedeck_ui/src/profile/mod.rs | 70++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Mcrates/notedeck_ui/src/profile/preview.rs | 2++
4 files changed, 55 insertions(+), 21 deletions(-)

diff --git a/crates/notedeck_columns/src/ui/profile/edit.rs b/crates/notedeck_columns/src/ui/profile/edit.rs @@ -45,7 +45,7 @@ impl<'a> EditProfileView<'a> { .id_salt(EditProfileView::scroll_id()) .stick_to_bottom(true) .show(ui, |ui| { - banner(ui, self.state.banner(), 188.0); + banner(ui, self.img_cache, self.jobs, self.state.banner(), 188.0); let padding = 24.0; notedeck_ui::padding(padding, ui, |ui| { diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -149,6 +149,8 @@ fn profile_body( ui.vertical(|ui| { let banner_resp = banner( ui, + note_context.img_cache, + note_context.jobs, profile .map(|p| p.record().profile()) .and_then(|p| p.and_then(|p| p.banner())), diff --git a/crates/notedeck_ui/src/profile/mod.rs b/crates/notedeck_ui/src/profile/mod.rs @@ -8,8 +8,12 @@ pub mod preview; pub use picture::ProfilePic; pub use preview::ProfilePreview; -use egui::{load::TexturePoll, Label, RichText}; -use notedeck::{IsFollowing, NostrName, NotedeckTextStyle}; +use egui::{Label, RichText, TextureHandle}; +use notedeck::media::images::ImageType; +use notedeck::media::AnimationMode; +use notedeck::{ + Images, IsFollowing, MediaJobSender, NostrName, NotedeckTextStyle, PointDimensions, +}; use crate::{app_images, colors, widgets::styled_button_toggleable}; @@ -93,38 +97,64 @@ pub fn about_section_widget<'a>(profile: Option<&'a ProfileRecord<'a>>) -> impl } } -pub fn banner_texture(ui: &mut egui::Ui, banner_url: &str) -> Option<egui::load::SizedTexture> { - // TODO: cache banner - if !banner_url.is_empty() { - let texture_load_res = - egui::Image::new(banner_url).load_for_size(ui.ctx(), ui.available_size()); - if let Ok(texture_poll) = texture_load_res { - match texture_poll { - TexturePoll::Pending { .. } => {} - TexturePoll::Ready { texture, .. } => return Some(texture), - } - } +/// Loads a banner texture using the shared media cache to prevent blocking. +#[profiling::function] +pub fn banner_texture<'a>( + ui: &mut egui::Ui, + cache: &'a mut Images, + jobs: &MediaJobSender, + banner_url: &str, + size: PointDimensions, +) -> Option<&'a TextureHandle> { + if banner_url.is_empty() { + return None; } - None + cache.latest_texture( + jobs, + ui, + banner_url, + ImageType::Content(Some(size.to_pixels(ui))), + AnimationMode::NoAnimation, + ) } -pub fn banner(ui: &mut egui::Ui, banner_url: Option<&str>, height: f32) -> egui::Response { - ui.add_sized([ui.available_size().x, height], |ui: &mut egui::Ui| { +/// Renders a profile banner via the cached loader so we avoid egui_extras overhead. +#[profiling::function] +pub fn banner( + ui: &mut egui::Ui, + cache: &mut Images, + jobs: &MediaJobSender, + banner_url: Option<&str>, + height: f32, +) -> egui::Response { + let x = ui.available_size().x; + ui.add_sized([x, height], |ui: &mut egui::Ui| { banner_url - .and_then(|url| banner_texture(ui, url)) + .and_then(|url| banner_texture(ui, cache, jobs, url, PointDimensions { x, y: height })) .map(|texture| { + let size = texture.size_vec2(); + let aspect_ratio = if size.y == 0.0 { 1.0 } else { size.x / size.y }; + notedeck::media::images::aspect_fill( ui, egui::Sense::hover(), - texture.id, - texture.size.x / texture.size.y, + texture.id(), + aspect_ratio, ) }) - .unwrap_or_else(|| ui.label("")) + .unwrap_or_else(|| empty_banner(ui)) }) } +/// Draws an empty banner placeholder while the image loads or is missing. +fn empty_banner(ui: &mut egui::Ui) -> egui::Response { + let (rect, response) = ui.allocate_exact_size(ui.available_size(), egui::Sense::hover()); + ui.painter() + .rect_filled(rect, 0.0, ui.visuals().faint_bg_color); + response +} + pub fn follow_button(following: IsFollowing) -> impl egui::Widget + 'static { move |ui: &mut egui::Ui| -> egui::Response { let (bg_color, text) = match following { diff --git a/crates/notedeck_ui/src/profile/preview.rs b/crates/notedeck_ui/src/profile/preview.rs @@ -65,6 +65,8 @@ impl egui::Widget for ProfilePreview<'_, '_> { ui.vertical(|ui| { banner( ui, + self.cache, + self.jobs, self.profile.record().profile().and_then(|p| p.banner()), 80.0, );