feat(image-picker): play animated thumbnails on hover
Hovering a GIF/WebP thumbnail in the picker now previews its animation: - The picker's Thumb gains a lazily-loaded frame sequence (via LoadAnimatedRGBA) fetched the first time an animatable (.gif/.webp) thumbnail is hovered; still images and other formats keep their single static thumbnail (no re-decode). - On hover the current frame is drawn on the ImGui clock; leaving the thumbnail returns it to the still frame-0 preview. - A clear-on-read flag (ImagePicker::consumeAnimationActive) is OR'd into ConsumeContactsAvatarAnimation so the render loop keeps drawing while a hover preview plays and idles otherwise. clearThumbs frees all frame textures too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -254,7 +254,13 @@ static void deleteAvatarLibraryFile(const std::string& path) {
|
|||||||
// UI-loop accessor: true if an animated avatar frame was drawn since the last call (clear-on-read), so
|
// UI-loop accessor: true if an animated avatar frame was drawn since the last call (clear-on-read), so
|
||||||
// the main render loop keeps producing frames while an animation plays and idles once it stops / the
|
// the main render loop keeps producing frames while an animation plays and idles once it stops / the
|
||||||
// contacts view is hidden. Declared in contacts_tab.h.
|
// contacts view is hidden. Declared in contacts_tab.h.
|
||||||
bool ConsumeContactsAvatarAnimation() { bool v = s_avatarAnimatedThisFrame; s_avatarAnimatedThisFrame = false; return v; }
|
bool ConsumeContactsAvatarAnimation() {
|
||||||
|
// Also consume the image picker's hover-preview animation flag (the picker renders within this tab).
|
||||||
|
bool picker = ImagePicker::consumeAnimationActive();
|
||||||
|
bool v = s_avatarAnimatedThisFrame || picker;
|
||||||
|
s_avatarAnimatedThisFrame = false;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
void RenderContactsTab(App* app)
|
void RenderContactsTab(App* app)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <cmath>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -229,12 +230,28 @@ public:
|
|||||||
dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 18), 6.0f * dp);
|
dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 18), 6.0f * dp);
|
||||||
// Only decode thumbnails for cells actually on-screen, and only a few per frame.
|
// Only decode thumbnails for cells actually on-screen, and only a few per frame.
|
||||||
const Thumb* t = ImGui::IsRectVisible(mn, mx) ? thumbFor(abs) : nullptr;
|
const Thumb* t = ImGui::IsRectVisible(mn, mx) ? thumbFor(abs) : nullptr;
|
||||||
if (t && t->tex) {
|
ImTextureID tex = t ? t->tex : 0;
|
||||||
|
int tw = t ? t->w : 0, thh = t ? t->h : 0;
|
||||||
|
// Hovering an animatable image (GIF/WebP) loads its full sequence (once) and plays it.
|
||||||
|
if (t && hov && isAnimatableName(s_images[i])) {
|
||||||
|
Thumb& mt = s_thumbs[abs]; // t is non-null => the entry exists
|
||||||
|
loadThumbAnim(mt, abs);
|
||||||
|
if (mt.frames.size() > 1 && mt.totalDur > 0.0f) {
|
||||||
|
s_animatingThisFrame = true;
|
||||||
|
double phase = std::fmod(ImGui::GetTime(), (double)mt.totalDur);
|
||||||
|
double acc = 0.0;
|
||||||
|
for (size_t k = 0; k < mt.delays.size() && k < mt.frames.size(); ++k) {
|
||||||
|
acc += mt.delays[k];
|
||||||
|
if (phase < acc) { tex = mt.frames[k]; tw = mt.aw; thh = mt.ah; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tex) {
|
||||||
float u0=0,v0=0,u1=1,v1=1; // centre-crop to a square
|
float u0=0,v0=0,u1=1,v1=1; // centre-crop to a square
|
||||||
if (t->w > t->h) { float m=(t->w-t->h)*0.5f/t->w; u0=m; u1=1-m; }
|
if (tw > thh) { float m=(tw-thh)*0.5f/tw; u0=m; u1=1-m; }
|
||||||
else if (t->h > t->w) { float m=(t->h-t->w)*0.5f/t->h; v0=m; v1=1-m; }
|
else if (thh > tw) { float m=(thh-tw)*0.5f/thh; v0=m; v1=1-m; }
|
||||||
const float ins = 3.0f * dp;
|
const float ins = 3.0f * dp;
|
||||||
dl->AddImageRounded(t->tex, ImVec2(mn.x+ins, mn.y+ins), ImVec2(mx.x-ins, mx.y-ins),
|
dl->AddImageRounded(tex, ImVec2(mn.x+ins, mn.y+ins), ImVec2(mx.x-ins, mx.y-ins),
|
||||||
ImVec2(u0,v0), ImVec2(u1,v1), IM_COL32_WHITE, 5.0f * dp);
|
ImVec2(u0,v0), ImVec2(u1,v1), IM_COL32_WHITE, 5.0f * dp);
|
||||||
} else {
|
} else {
|
||||||
ImVec2 cc(mn.x + cell*0.5f, mn.y + cell*0.5f);
|
ImVec2 cc(mn.x + cell*0.5f, mn.y + cell*0.5f);
|
||||||
@@ -291,8 +308,35 @@ public:
|
|||||||
if (!s_open) clearThumbs(); // release GL textures the frame the picker dismisses
|
if (!s_open) clearThumbs(); // release GL textures the frame the picker dismisses
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Clear-on-read: true if an animated thumbnail was playing this frame (mouse over it), so the render
|
||||||
|
// loop keeps drawing while a hover-preview animates. Consumed by ConsumeContactsAvatarAnimation.
|
||||||
|
static bool consumeAnimationActive() { bool v = s_animatingThisFrame; s_animatingThisFrame = false; return v; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct Thumb { ImTextureID tex = 0; int w = 0, h = 0; bool tried = false; };
|
struct Thumb {
|
||||||
|
ImTextureID tex = 0; int w = 0, h = 0; bool tried = false; // static frame-0 thumbnail
|
||||||
|
std::vector<ImTextureID> frames; // full sequence (loaded on hover)
|
||||||
|
std::vector<float> delays; int aw = 0, ah = 0; float totalDur = 0.0f; bool animTried = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load the full frame sequence for an animated image on first hover (still images stay single-frame).
|
||||||
|
static void loadThumbAnim(Thumb& th, const std::string& path) {
|
||||||
|
if (th.animTried) return;
|
||||||
|
th.animTried = true;
|
||||||
|
util::AnimFrames af;
|
||||||
|
if (util::LoadAnimatedRGBA(path.c_str(), kThumbMaxFrames, af) && af.frames.size() > 1) {
|
||||||
|
th.aw = af.w; th.ah = af.h;
|
||||||
|
for (size_t i = 0; i < af.frames.size(); ++i) {
|
||||||
|
ImTextureID t = 0;
|
||||||
|
if (util::CreateRawTexture(af.frames[i].data(), af.w, af.h, false, &t)) {
|
||||||
|
th.frames.push_back(t);
|
||||||
|
th.delays.push_back(i < af.delaysSec.size() ? af.delaysSec[i] : 0.1f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (float d : th.delays) th.totalDur += d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static bool isImageName(const std::string& name) {
|
static bool isImageName(const std::string& name) {
|
||||||
auto lower = name;
|
auto lower = name;
|
||||||
@@ -307,6 +351,14 @@ private:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only these can hold multiple frames in our decoders; skip the hover re-decode for other formats.
|
||||||
|
static bool isAnimatableName(const std::string& name) {
|
||||||
|
auto lo = name;
|
||||||
|
for (char& c : lo) c = (char)std::tolower((unsigned char)c);
|
||||||
|
auto ends = [&](const char* e){ std::string s(e); return lo.size()>s.size() && lo.compare(lo.size()-s.size(), s.size(), s)==0; };
|
||||||
|
return ends(".gif") || ends(".webp");
|
||||||
|
}
|
||||||
|
|
||||||
static std::string defaultStartDir() {
|
static std::string defaultStartDir() {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
@@ -356,7 +408,10 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void clearThumbs() {
|
static void clearThumbs() {
|
||||||
for (auto& kv : s_thumbs) if (kv.second.tex) util::DestroyTexture(kv.second.tex);
|
for (auto& kv : s_thumbs) {
|
||||||
|
if (kv.second.tex) util::DestroyTexture(kv.second.tex);
|
||||||
|
for (ImTextureID f : kv.second.frames) if (f) util::DestroyTexture(f);
|
||||||
|
}
|
||||||
s_thumbs.clear();
|
s_thumbs.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -389,8 +444,10 @@ private:
|
|||||||
|
|
||||||
static constexpr int kThumbPx = 128; // max thumbnail dimension (downscaled)
|
static constexpr int kThumbPx = 128; // max thumbnail dimension (downscaled)
|
||||||
static constexpr int kLoadsPerFrame = 6; // decode budget per frame (spreads a big folder over frames)
|
static constexpr int kLoadsPerFrame = 6; // decode budget per frame (spreads a big folder over frames)
|
||||||
|
static constexpr int kThumbMaxFrames = 300; // cap frames per hovered animation
|
||||||
|
|
||||||
static inline bool s_open = false;
|
static inline bool s_open = false;
|
||||||
|
static inline bool s_animatingThisFrame = false; // a hover-preview animation is playing
|
||||||
static inline int s_loadedThisFrame = 0;
|
static inline int s_loadedThisFrame = 0;
|
||||||
static inline std::string s_dir;
|
static inline std::string s_dir;
|
||||||
static inline std::string s_selected;
|
static inline std::string s_selected;
|
||||||
|
|||||||
Reference in New Issue
Block a user