fix(contacts): don't keep the app awake for off-screen animated avatars
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<int>(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;
|
||||
|
||||
Reference in New Issue
Block a user