From 137147921cd9aad213afddd24739eed79c22309e Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 15 Jul 2026 11:26:31 -0500 Subject: [PATCH] fix(contacts): don't keep the app awake for off-screen animated avatars MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adversarial review found that the contact list/table loops (no clipper) call drawContactAvatar for every row, so an animated avatar scrolled out of the list/table viewport still flagged the render loop as animating — pinning the app at vsync-rate redraw instead of idling (power drain). currentAvatarFrame now takes an onScreen flag: off-screen it shows frame 0 and does NOT set the keep-redrawing flag. The list and table pass ImGui::IsRectVisible for the row/avatar; the preview passes true; the library grid already yields a null texture for culled cells. (Two other findings — stb GIF peak host RAM and the session texture cache — were reviewed and judged bounded/local-only.) Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/contacts_tab.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/ui/windows/contacts_tab.cpp b/src/ui/windows/contacts_tab.cpp index 452bfe2..3e573d6 100644 --- a/src/ui/windows/contacts_tab.cpp +++ b/src/ui/windows/contacts_tab.cpp @@ -108,11 +108,13 @@ static const AvatarTex* getAvatarTexture(const std::string& path) { } // The texture to draw for `t` right now: a still's single frame, or — when the animate-avatars setting -// is on — the animated frame for the current ImGui clock time (and flag that an animation is live, so -// the render loop keeps producing frames instead of idling). -static ImTextureID currentAvatarFrame(const AvatarTex* t) { +// is on AND the avatar is actually on-screen — the animated frame for the current ImGui clock time (and +// flag that an animation is live, so the render loop keeps producing frames instead of idling). Passing +// onScreen=false (an avatar scrolled out of a list/table viewport) shows frame 0 and does NOT flag, so a +// culled animated avatar can't pin the app awake. +static ImTextureID currentAvatarFrame(const AvatarTex* t, bool onScreen = true) { if (!t || t->frames.empty()) return 0; - if (t->frames.size() > 1 && s_animateAvatars && t->totalDur > 0.0f) { + if (onScreen && t->frames.size() > 1 && s_animateAvatars && t->totalDur > 0.0f) { s_avatarAnimatedThisFrame = true; double phase = std::fmod(ImGui::GetTime(), (double)t->totalDur); double acc = 0.0; @@ -136,11 +138,11 @@ static void invalidateAvatarTexture(const std::string& path) { // Material icon in a tinted circle, or — the default — the Z/T type badge. static void drawContactAvatar(ImDrawList* dl, ImVec2 c, float r, const data::AddressBookEntry& e, bool shielded, ImU32 typeCol, bool light, float dp, - ImFont* letterFont, ImFont* iconFont) { + ImFont* letterFont, ImFont* iconFont, bool onScreen = true) { const std::string& av = e.avatar; if (av.rfind("img:", 0) == 0) { const AvatarTex* t = getAvatarTexture(av.substr(4)); - ImTextureID tex = currentAvatarFrame(t); + ImTextureID tex = currentAvatarFrame(t, onScreen); if (tex) { float u0 = 0, v0 = 0, u1 = 1, v1 = 1; // centre-crop to a square so the circle isn't stretched if (t->w > t->h) { float m = (t->w - t->h) * 0.5f / t->w; u0 = m; u1 = 1 - m; } @@ -978,9 +980,10 @@ void RenderContactsTab(App* app) if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Right)) { s_selected_index = static_cast(i); s_confirm_delete_idx = -1; openContextMenu = true; } + bool tAvOnScreen = ImGui::IsRectVisible(tAvP, ImVec2(tAvP.x + tAvR * 2.0f, tAvP.y + tLineH)); drawContactAvatar(ImGui::GetWindowDrawList(), ImVec2(tAvP.x + tAvR, tAvP.y + tLineH * 0.5f), tAvR, entry, shielded, typeColor(shielded), lightTheme, dp, - material::Type().caption(), material::Type().iconSmall()); + material::Type().caption(), material::Type().iconSmall(), tAvOnScreen); ImGui::TableNextColumn(); ImGui::PushFont(material::Type().subtitle2()); ImGui::TextColored(ImGui::ColorConvertU32ToFloat4(typeColor(shielded)), "%s", shielded ? "Z" : "T"); @@ -1069,7 +1072,10 @@ void RenderContactsTab(App* app) const bool shielded = isShieldedAddr(entry.address); const ImU32 tu = typeColor(shielded); const ImVec2 avC(mn.x + pad + avR, mn.y + rowH * 0.5f); - drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF); + // Only animate when the row is actually on-screen, so a scrolled-off animated avatar + // doesn't hold the app awake (this loop has no clipper). + const bool rowOnScreen = ImGui::IsRectVisible(mn, mx); + drawContactAvatar(dl, avC, avR, entry, shielded, tu, lightTheme, dp, lblF, icoF, rowOnScreen); // Label (line 1) + muted address (line 2) — reserve trailing room for the globe + actions. const float actHit = icoF->LegacySize + 8.0f * dp; // per-action square hit area const float globeW = entry.isGlobal() ? (icoF->LegacySize + 8.0f * dp) : 0.0f;