notedeck

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

commit e87b6f1905eec21f40fb54af8ad5ffa33e6c1a2f
parent 5cb0911d7e9c5543009515f1ab68205989b3f398
Author: William Casarin <jb55@jb55.com>
Date:   Thu,  5 Jun 2025 11:51:07 -0700

chrome: collapsible side panel

This implements the initial logic that makes the side panel collapsible.

Since we don't have a proper hamburger control, we do the same thing we
do on iOS for now.

Diffstat:
Mcrates/notedeck/src/app.rs | 1+
Mcrates/notedeck_chrome/src/chrome.rs | 27++++++++++++++++++++++++---
Mcrates/notedeck_columns/src/app.rs | 92++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mcrates/notedeck_columns/src/nav.rs | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Mcrates/notedeck_columns/src/ui/column/header.rs | 130++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/notedeck_ui/src/note/mod.rs | 9+++++----
Mcrates/notedeck_ui/src/profile/picture.rs | 80+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
7 files changed, 282 insertions(+), 136 deletions(-)

diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -19,6 +19,7 @@ use tracing::{error, info}; pub enum AppAction { Note(NoteAction), + ToggleChrome, } pub trait App { diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -16,12 +16,22 @@ use notedeck_ui::{AnimationHelper, ProfilePic}; static ICON_WIDTH: f32 = 40.0; pub static ICON_EXPANSION_MULTIPLE: f32 = 1.2; -#[derive(Default)] pub struct Chrome { active: i32, + open: bool, apps: Vec<NotedeckApp>, } +impl Default for Chrome { + fn default() -> Self { + Self { + active: 0, + open: true, + apps: vec![], + } + } +} + pub enum ChromePanelAction { Support, Settings, @@ -85,6 +95,10 @@ impl Chrome { Chrome::default() } + pub fn toggle(&mut self) { + self.open = !self.open; + } + pub fn add_app(&mut self, app: NotedeckApp) { self.apps.push(app); } @@ -132,8 +146,11 @@ impl Chrome { let mut got_action: Option<ChromePanelAction> = None; let side_panel_width: f32 = 70.0; + let open_id = egui::Id::new("chrome_open"); + let amt_open = ui.ctx().animate_bool(open_id, self.open) * side_panel_width; + StripBuilder::new(ui) - .size(Size::exact(side_panel_width)) // collapsible sidebar + .size(Size::exact(amt_open)) // collapsible sidebar .size(Size::remainder()) // the main app contents .clip(true) .horizontal(|mut strip| { @@ -294,7 +311,7 @@ impl Chrome { if ui.add(expand_side_panel_button()).clicked() { //self.active = (self.active + 1) % (self.apps.len() as i32); - // TODO: collapse sidebar ? + self.open = !self.open; } ui.add_space(4.0); @@ -492,6 +509,10 @@ fn chrome_handle_app_action( ui: &mut egui::Ui, ) { match action { + AppAction::ToggleChrome => { + chrome.toggle(); + } + AppAction::Note(note_action) => { chrome.switch_to_columns(); let Some(columns) = chrome.get_columns() else { diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -3,7 +3,7 @@ use crate::{ column::Columns, decks::{Decks, DecksCache, FALLBACK_PUBKEY}, draft::Drafts, - nav, + nav::{self, ProcessNavResult}, route::Route, storage, subscriptions::{SubKind, Subscriptions}, @@ -340,15 +340,21 @@ fn process_message(damus: &mut Damus, ctx: &mut AppContext<'_>, relay: &str, msg } } -fn render_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { - if notedeck::ui::is_narrow(ui.ctx()) { - render_damus_mobile(damus, app_ctx, ui); +fn render_damus( + damus: &mut Damus, + app_ctx: &mut AppContext<'_>, + ui: &mut egui::Ui, +) -> Option<AppAction> { + let app_action = if notedeck::ui::is_narrow(ui.ctx()) { + render_damus_mobile(damus, app_ctx, ui) } else { - render_damus_desktop(damus, app_ctx, ui); - } + render_damus_desktop(damus, app_ctx, ui) + }; // We use this for keeping timestamps and things up to date ui.ctx().request_repaint_after(Duration::from_secs(1)); + + app_action } /* @@ -518,17 +524,32 @@ fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { */ #[profiling::function] -fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { +fn render_damus_mobile( + app: &mut Damus, + app_ctx: &mut AppContext<'_>, + ui: &mut egui::Ui, +) -> Option<AppAction> { //let routes = app.timelines[0].routes.clone(); let mut rect = ui.available_rect_before_wrap(); + let mut app_action: Option<AppAction> = None; + + if !app.columns(app_ctx.accounts).columns().is_empty() { + let r = nav::render_nav(0, ui.available_rect_before_wrap(), app, app_ctx, ui) + .process_render_nav_response(app, app_ctx, ui); + if let Some(r) = &r { + match r { + ProcessNavResult::SwitchOccurred => { + if !app.tmp_columns { + storage::save_decks_cache(app_ctx.path, &app.decks_cache); + } + } - if !app.columns(app_ctx.accounts).columns().is_empty() - && nav::render_nav(0, ui.available_rect_before_wrap(), app, app_ctx, ui) - .process_render_nav_response(app, app_ctx, ui) - && !app.tmp_columns - { - storage::save_decks_cache(app_ctx.path, &app.decks_cache); + ProcessNavResult::PfpClicked => { + app_action = Some(AppAction::ToggleChrome); + } + } + } } rect.min.x = rect.max.x - 100.0; @@ -549,10 +570,16 @@ fn render_damus_mobile(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut e router.route_to(Route::ComposeNote); } } + + app_action } #[profiling::function] -fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut egui::Ui) { +fn render_damus_desktop( + app: &mut Damus, + app_ctx: &mut AppContext<'_>, + ui: &mut egui::Ui, +) -> Option<AppAction> { let screen_size = ui.ctx().screen_rect().width(); let calc_panel_width = (screen_size / get_active_columns(app_ctx.accounts, &app.decks_cache).num_columns() as f32) @@ -566,16 +593,22 @@ fn render_damus_desktop(app: &mut Damus, app_ctx: &mut AppContext<'_>, ui: &mut }; ui.spacing_mut().item_spacing.x = 0.0; + if need_scroll { - egui::ScrollArea::horizontal().show(ui, |ui| { - timelines_view(ui, panel_sizes, app, app_ctx); - }); + egui::ScrollArea::horizontal() + .show(ui, |ui| timelines_view(ui, panel_sizes, app, app_ctx)) + .inner } else { - timelines_view(ui, panel_sizes, app, app_ctx); + timelines_view(ui, panel_sizes, app, app_ctx) } } -fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut AppContext<'_>) { +fn timelines_view( + ui: &mut egui::Ui, + sizes: Size, + app: &mut Damus, + ctx: &mut AppContext<'_>, +) -> Option<AppAction> { let num_cols = get_active_columns(ctx.accounts, &app.decks_cache).num_columns(); let mut side_panel_action: Option<nav::SwitchingAction> = None; let mut responses = Vec::with_capacity(num_cols); @@ -654,9 +687,20 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx); } + let mut app_action: Option<AppAction> = None; + for response in responses { - let save = response.process_render_nav_response(app, ctx, ui); - save_cols = save_cols || save; + let nav_result = response.process_render_nav_response(app, ctx, ui); + + if let Some(nr) = &nav_result { + match nr { + ProcessNavResult::SwitchOccurred => save_cols = true, + + ProcessNavResult::PfpClicked => { + app_action = Some(AppAction::ToggleChrome); + } + } + } } if app.tmp_columns { @@ -666,6 +710,8 @@ fn timelines_view(ui: &mut egui::Ui, sizes: Size, app: &mut Damus, ctx: &mut App if save_cols { storage::save_decks_cache(ctx.path, &app.decks_cache); } + + app_action } impl notedeck::App for Damus { @@ -677,9 +723,7 @@ impl notedeck::App for Damus { */ update_damus(self, ctx, ui.ctx()); - render_damus(self, ctx, ui); - - None + render_damus(self, ctx, ui) } } diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -34,10 +34,24 @@ use notedeck::{ use notedeck_ui::View; use tracing::error; +/// The result of processing a nav response +pub enum ProcessNavResult { + SwitchOccurred, + PfpClicked, +} + +impl ProcessNavResult { + pub fn switch_occurred(&self) -> bool { + matches!(self, Self::SwitchOccurred) + } +} + #[allow(clippy::enum_variant_names)] pub enum RenderNavAction { Back, RemoveColumn, + /// The response when the user interacts with a pfp in the nav header + PfpClicked, PostAction(NewPostAction), NoteAction(NoteAction), ProfileAction(ProfileAction), @@ -144,11 +158,10 @@ impl RenderNavResponse { app: &mut Damus, ctx: &mut AppContext<'_>, ui: &mut egui::Ui, - ) -> bool { + ) -> Option<ProcessNavResult> { match self.response { NotedeckNavResponse::Popup(nav_action) => { - process_popup_resp(*nav_action, app, ctx, ui, self.column); - false + process_popup_resp(*nav_action, app, ctx, ui, self.column) } NotedeckNavResponse::Nav(nav_response) => { process_nav_resp(app, ctx, ui, *nav_response, self.column) @@ -163,10 +176,10 @@ fn process_popup_resp( ctx: &mut AppContext<'_>, ui: &mut egui::Ui, col: usize, -) -> bool { - let mut switching_occured = false; +) -> Option<ProcessNavResult> { + let mut process_result: Option<ProcessNavResult> = None; if let Some(nav_action) = action.response { - switching_occured = process_render_nav_action(app, ctx, ui, col, nav_action); + process_result = process_render_nav_action(app, ctx, ui, col, nav_action); } if let Some(NavAction::Returned) = action.action { @@ -177,7 +190,7 @@ fn process_popup_resp( column.sheet_router.navigating = false; } - switching_occured + process_result } fn process_nav_resp( @@ -186,13 +199,13 @@ fn process_nav_resp( ui: &mut egui::Ui, response: NavResponse<Option<RenderNavAction>>, col: usize, -) -> bool { - let mut switching_occured: bool = false; +) -> Option<ProcessNavResult> { + let mut process_result: Option<ProcessNavResult> = None; if let Some(action) = response.response.or(response.title_response) { // start returning when we're finished posting - switching_occured = process_render_nav_action(app, ctx, ui, col, action); + process_result = process_render_nav_action(app, ctx, ui, col, action); } if let Some(action) = response.action { @@ -210,7 +223,7 @@ fn process_nav_resp( } }; - switching_occured = true; + process_result = Some(ProcessNavResult::SwitchOccurred); } NavAction::Navigated => { @@ -219,7 +232,8 @@ fn process_nav_resp( if cur_router.is_replacing() { cur_router.remove_previous_routes(); } - switching_occured = true; + + process_result = Some(ProcessNavResult::SwitchOccurred); } NavAction::Dragging => {} @@ -229,11 +243,15 @@ fn process_nav_resp( } } - switching_occured + process_result } pub enum RouterAction { GoBack, + /// We clicked on a pfp in a route. We currently don't carry any + /// information about the pfp since we only use it for toggling the + /// chrome atm + PfpClicked, RouteTo(Route, RouterType), } @@ -247,7 +265,7 @@ impl RouterAction { self, stack_router: &mut Router<Route>, sheet_router: &mut SingletonRouter<Route>, - ) { + ) -> Option<ProcessNavResult> { match self { RouterAction::GoBack => { if sheet_router.route().is_some() { @@ -255,10 +273,21 @@ impl RouterAction { } else { stack_router.go_back(); } + + None } + + RouterAction::PfpClicked => Some(ProcessNavResult::PfpClicked), + RouterAction::RouteTo(route, router_type) => match router_type { - RouterType::Sheet => sheet_router.route_to(route), - RouterType::Stack => stack_router.route_to(route), + RouterType::Sheet => { + sheet_router.route_to(route); + None + } + RouterType::Stack => { + stack_router.route_to(route); + None + } }, } } @@ -278,9 +307,10 @@ fn process_render_nav_action( ui: &mut egui::Ui, col: usize, action: RenderNavAction, -) -> bool { +) -> Option<ProcessNavResult> { let router_action = match action { RenderNavAction::Back => Some(RouterAction::GoBack), + RenderNavAction::PfpClicked => Some(RouterAction::PfpClicked), RenderNavAction::RemoveColumn => { let kinds_to_pop = app.columns_mut(ctx.accounts).delete_column(col); @@ -291,7 +321,7 @@ fn process_render_nav_action( } } - return true; + return Some(ProcessNavResult::SwitchOccurred); } RenderNavAction::PostAction(new_post_action) => { @@ -326,7 +356,11 @@ fn process_render_nav_action( } RenderNavAction::SwitchingAction(switching_action) => { - return switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx); + if switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx) { + return Some(ProcessNavResult::SwitchOccurred); + } else { + return None; + } } RenderNavAction::ProfileAction(profile_action) => profile_action.process( &mut app.view_state.pubkey_to_profile_state, @@ -342,10 +376,11 @@ fn process_render_nav_action( let cols = get_active_columns_mut(ctx.accounts, &mut app.decks_cache).column_mut(col); let router = &mut cols.router; let sheet_router = &mut cols.sheet_router; - action.process(router, sheet_router); - } - false + action.process(router, sheet_router) + } else { + None + } } fn render_nav_body( diff --git a/crates/notedeck_columns/src/ui/column/header.rs b/crates/notedeck_columns/src/ui/column/header.rs @@ -8,8 +8,7 @@ use crate::{ ui::{self}, }; -use egui::Margin; -use egui::{RichText, Stroke, UiBuilder}; +use egui::{Margin, Response, RichText, Sense, Stroke, UiBuilder}; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; use notedeck::{Images, NotedeckTextStyle}; @@ -84,8 +83,10 @@ impl<'a> NavTitle<'a> { let title_resp = self.title(ui, self.routes.last().unwrap(), back_button_resp.is_some()); if let Some(resp) = title_resp { + tracing::debug!("got title response {resp:?}"); match resp { TitleResponse::RemoveColumn => Some(RenderNavAction::RemoveColumn), + TitleResponse::PfpClicked => Some(RenderNavAction::PfpClicked), TitleResponse::MoveColumn(to_index) => { let from = self.col_id; Some(RenderNavAction::SwitchingAction(SwitchingAction::Columns( @@ -94,6 +95,7 @@ impl<'a> NavTitle<'a> { } } } else if back_button_resp.is_some_and(|r| r.clicked()) { + tracing::debug!("render nav action back"); Some(RenderNavAction::Back) } else { None @@ -395,89 +397,95 @@ impl<'a> NavTitle<'a> { .get_profile_by_pubkey(txn, pubkey) .as_ref() .ok() - .and_then(move |p| Some(ProfilePic::from_profile(self.img_cache, p)?.size(pfp_size))) + .and_then(move |p| { + Some( + ProfilePic::from_profile(self.img_cache, p)? + .size(pfp_size) + .sense(Sense::click()), + ) + }) } - fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) { + fn timeline_pfp(&mut self, ui: &mut egui::Ui, id: &TimelineKind, pfp_size: f32) -> Response { let txn = Transaction::new(self.ndb).unwrap(); if let Some(mut pfp) = id .pubkey() .and_then(|pk| self.pubkey_pfp(&txn, pk.bytes(), pfp_size)) { - ui.add(&mut pfp); + ui.add(&mut pfp) } else { ui.add( &mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()) - .size(pfp_size), - ); + .size(pfp_size) + .sense(Sense::click()), + ) } } - fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) { + fn title_pfp(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) -> Option<Response> { match top { Route::Timeline(kind) => match kind { - TimelineKind::Hashtag(_ht) => { + TimelineKind::Hashtag(_ht) => Some( ui.add( egui::Image::new(egui::include_image!( "../../../../../assets/icons/hashtag_icon_4x.png" )) .fit_to_exact_size(egui::vec2(pfp_size, pfp_size)), - ); - } + ), + ), - TimelineKind::Profile(pubkey) => { - self.show_profile(ui, pubkey, pfp_size); - } + TimelineKind::Profile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)), TimelineKind::Thread(_) => { // no pfp for threads + None } TimelineKind::Search(_sq) => { // TODO: show author pfp if author field set? - ui.add(ui::side_panel::search_button()); + Some(ui.add(ui::side_panel::search_button())) } TimelineKind::Universe | TimelineKind::Algo(_) | TimelineKind::Notifications(_) | TimelineKind::Generic(_) - | TimelineKind::List(_) => { - self.timeline_pfp(ui, kind, pfp_size); - } + | TimelineKind::List(_) => Some(self.timeline_pfp(ui, kind, pfp_size)), }, - Route::Reply(_) => {} - Route::Quote(_) => {} - Route::Accounts(_as) => {} - Route::ComposeNote => {} - Route::AddColumn(_add_col_route) => {} - Route::Support => {} - Route::Relays => {} - Route::NewDeck => {} - Route::EditDeck(_) => {} - Route::EditProfile(pubkey) => { - self.show_profile(ui, pubkey, pfp_size); - } - Route::Search => { - ui.add(ui::side_panel::search_button()); - } - Route::Wallet(_) => {} - Route::CustomizeZapAmount(_) => {} + Route::Reply(_) => None, + Route::Quote(_) => None, + Route::Accounts(_as) => None, + Route::ComposeNote => None, + Route::AddColumn(_add_col_route) => None, + Route::Support => None, + Route::Relays => None, + Route::NewDeck => None, + Route::EditDeck(_) => None, + Route::EditProfile(pubkey) => Some(self.show_profile(ui, pubkey, pfp_size)), + Route::Search => Some(ui.add(ui::side_panel::search_button())), + Route::Wallet(_) => None, + Route::CustomizeZapAmount(_) => None, } } - fn show_profile(&mut self, ui: &mut egui::Ui, pubkey: &Pubkey, pfp_size: f32) { + fn show_profile( + &mut self, + ui: &mut egui::Ui, + pubkey: &Pubkey, + pfp_size: f32, + ) -> egui::Response { let txn = Transaction::new(self.ndb).unwrap(); if let Some(mut pfp) = self.pubkey_pfp(&txn, pubkey.bytes(), pfp_size) { - ui.add(&mut pfp); + ui.add(&mut pfp) } else { ui.add( &mut ProfilePic::new(self.img_cache, notedeck::profile::no_pfp_url()) - .size(pfp_size), - ); - }; + .size(pfp_size) + .sense(Sense::click()), + ) + } } fn title_label_value(title: &str) -> egui::Label { @@ -489,27 +497,26 @@ impl<'a> NavTitle<'a> { let column_title = top.title(); match &column_title { - ColumnTitle::Simple(title) => { - ui.add(Self::title_label_value(title)); - } + ColumnTitle::Simple(title) => ui.add(Self::title_label_value(title)), ColumnTitle::NeedsDb(need_db) => { let txn = Transaction::new(self.ndb).unwrap(); let title = need_db.title(&txn, self.ndb); - ui.add(Self::title_label_value(title)); + ui.add(Self::title_label_value(title)) } }; } fn title(&mut self, ui: &mut egui::Ui, top: &Route, navigating: bool) -> Option<TitleResponse> { - if !navigating { - self.title_presentation(ui, top, 32.0); - } + let title_r = if !navigating { + self.title_presentation(ui, top, 32.0) + } else { + None + }; ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { if navigating { - self.title_presentation(ui, top, 32.0); - None + self.title_presentation(ui, top, 32.0) } else { let move_col = self.move_button_section(ui); let remove_col = self.delete_button_section(ui); @@ -523,16 +530,37 @@ impl<'a> NavTitle<'a> { } }) .inner + .or(title_r) } - fn title_presentation(&mut self, ui: &mut egui::Ui, top: &Route, pfp_size: f32) { - self.title_pfp(ui, top, pfp_size); + fn title_presentation( + &mut self, + ui: &mut egui::Ui, + top: &Route, + pfp_size: f32, + ) -> Option<TitleResponse> { + let pfp_r = self.title_pfp(ui, top, pfp_size); + + if pfp_r.as_ref().is_some_and(|r| r.hovered()) { + notedeck_ui::show_pointer(ui); + } + self.title_label(ui, top); + + pfp_r.and_then(|r| { + if r.clicked() { + Some(TitleResponse::PfpClicked) + } else { + None + } + }) } } +#[derive(Debug)] enum TitleResponse { RemoveColumn, + PfpClicked, MoveColumn(usize), } diff --git a/crates/notedeck_ui/src/note/mod.rs b/crates/notedeck_ui/src/note/mod.rs @@ -269,6 +269,11 @@ impl<'a, 'd> NoteView<'a, 'd> { let pfp_resp = ui.put(rect, &mut pfp); action = action.or(pfp.action); + + if resp.hovered() || resp.clicked() { + crate::show_pointer(ui); + } + pfp_resp.on_hover_ui_at_pointer(|ui| { ui.set_max_width(300.0); ui.add(ProfilePreview::new( @@ -277,10 +282,6 @@ impl<'a, 'd> NoteView<'a, 'd> { )); }); - if resp.hovered() || resp.clicked() { - crate::show_pointer(ui); - } - resp } diff --git a/crates/notedeck_ui/src/profile/picture.rs b/crates/notedeck_ui/src/profile/picture.rs @@ -9,13 +9,14 @@ pub struct ProfilePic<'cache, 'url> { cache: &'cache mut Images, url: &'url str, size: f32, + sense: Sense, border: Option<Stroke>, pub action: Option<MediaAction>, } impl egui::Widget for &mut ProfilePic<'_, '_> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { - let inner = render_pfp(ui, self.cache, self.url, self.size, self.border); + let inner = render_pfp(ui, self.cache, self.url, self.size, self.border, self.sense); self.action = inner.inner; @@ -26,8 +27,11 @@ impl egui::Widget for &mut ProfilePic<'_, '_> { impl<'cache, 'url> ProfilePic<'cache, 'url> { pub fn new(cache: &'cache mut Images, url: &'url str) -> Self { let size = Self::default_size() as f32; + let sense = Sense::hover(); + ProfilePic { cache, + sense, url, size, border: None, @@ -35,6 +39,11 @@ impl<'cache, 'url> ProfilePic<'cache, 'url> { } } + pub fn sense(mut self, sense: Sense) -> Self { + self.sense = sense; + self + } + pub fn border_stroke(ui: &egui::Ui) -> Stroke { Stroke::new(4.0, ui.visuals().panel_fill) } @@ -98,6 +107,7 @@ fn render_pfp( url: &str, ui_size: f32, border: Option<Stroke>, + sense: Sense, ) -> InnerResponse<Option<MediaAction>> { // We will want to downsample these so it's not blurry on hi res displays let img_size = 128u32; @@ -105,39 +115,39 @@ fn render_pfp( let cache_type = supported_mime_hosted_at_url(&mut img_cache.urls, url) .unwrap_or(notedeck::MediaCacheType::Image); - egui::Frame::NONE.show(ui, |ui| { - let cur_state = get_render_state( - ui.ctx(), - img_cache, - cache_type, - url, - ImageType::Profile(img_size), - ); - - match cur_state.texture_state { - notedeck::TextureState::Pending => { - paint_circle(ui, ui_size, border); - None - } - notedeck::TextureState::Error(e) => { - paint_circle(ui, ui_size, border); - show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}")); + let cur_state = get_render_state( + ui.ctx(), + img_cache, + cache_type, + url, + ImageType::Profile(img_size), + ); + + match cur_state.texture_state { + notedeck::TextureState::Pending => { + egui::InnerResponse::new(None, paint_circle(ui, ui_size, border, sense)) + } + notedeck::TextureState::Error(e) => { + let r = paint_circle(ui, ui_size, border, sense); + show_one_error_message(ui, &format!("Failed to fetch profile at url {url}: {e}")); + egui::InnerResponse::new( Some(MediaAction::FetchImage { url: url.to_owned(), cache_type, no_pfp_promise: fetch_no_pfp_promise(ui.ctx(), img_cache.get_cache(cache_type)), - }) - } - notedeck::TextureState::Loaded(textured_image) => { - let texture_handle = handle_repaint( - ui, - retrieve_latest_texture(url, cur_state.gifs, textured_image), - ); - pfp_image(ui, texture_handle, ui_size, border); - None - } + }), + r, + ) } - }) + notedeck::TextureState::Loaded(textured_image) => { + let texture_handle = handle_repaint( + ui, + retrieve_latest_texture(url, cur_state.gifs, textured_image), + ); + + egui::InnerResponse::new(None, pfp_image(ui, texture_handle, ui_size, border, sense)) + } + } } #[profiling::function] @@ -146,8 +156,9 @@ fn pfp_image( img: &TextureHandle, size: f32, border: Option<Stroke>, + sense: Sense, ) -> egui::Response { - let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); + let (rect, response) = ui.allocate_at_least(vec2(size, size), sense); if let Some(stroke) = border { draw_bg_border(ui, rect.center(), size, stroke); } @@ -156,8 +167,13 @@ fn pfp_image( response } -fn paint_circle(ui: &mut egui::Ui, size: f32, border: Option<Stroke>) -> egui::Response { - let (rect, response) = ui.allocate_at_least(vec2(size, size), Sense::hover()); +fn paint_circle( + ui: &mut egui::Ui, + size: f32, + border: Option<Stroke>, + sense: Sense, +) -> egui::Response { + let (rect, response) = ui.allocate_at_least(vec2(size, size), sense); if let Some(stroke) = border { draw_bg_border(ui, rect.center(), size, stroke);