Commit Graph
11 Commits
Author SHA1 Message Date
DanSandClaude Opus 4.8 01b59d24db perf(image-picker): cull off-screen thumbnail cells
The image picker laid out and drew every cell in the directory and image
grids, including the many cells scrolled out of view.

- A cell whose rectangle is not visible now reserves the same space with
  a dummy of the cell size, so the scroll extent is unchanged, and skips
  the hit-test and draw.

No visible change; only off-screen cells are skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-13 09:24:51 -05:00
DanSandClaude Opus 5.5 4a2a1bdd54 perf(image-picker): decode thumbnails off the UI thread
The image picker decoded up to 6 full-size images per frame on the UI
thread to make its thumbnails (1.5-2.7 s per frame with 12 MP photos,
until the visible grid filled) and read every .gif/.webp a second time
just to see whether it animates. Thumbnails were freed only on navigate
or close, so a large folder and every hovered animation (up to ~20 MB
each) stayed resident.

Thumbnails now decode on util::StartImageDecode workers (at most 2 at
once, so the top of the visible grid fills first) and the UI thread
uploads up to 8 small textures per frame. The animation badge comes from
the same decode, with no second read. Hover previews get their own
2-slot lane, and the picker asks for a frame every 100 ms while anything
is pending; under idle pacing a deferred thumbnail used to wait for input
or the 2 s tick.

Behaviour changes:
- thumbnails and hover frames are LRU-capped at 64 MiB per folder;
  evicted ones re-decode when they are needed again;
- leaving an animated thumbnail before its preview finishes decoding
  cancels it (re-hover restarts it); a finished preview is kept;
- animated WebP files get a real thumbnail instead of the placeholder;
- images above the decoder's limits show the placeholder.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 21:09:48 -05:00
DanSandClaude Opus 4.8 6f0f95f4fb fix(image-picker): avoid shutdown UB in the async decode counter
Adversarial review of the off-thread decode found the one real defect: a
detached decode worker decremented the static s_animInFlight counter, which at
process exit could run after static destruction begins (shutdown UB). Make the
counter a heap std::shared_ptr<atomic> the worker co-owns, so it safely outlives
teardown; the worker now touches only heap objects it holds a share of. Also
noted s_animatingThisFrame is intentionally main-thread-only.

(Review confirmed the rest: worker never touches the thumb map, done
release/acquire publishes the frames, GL upload stays on the UI thread, stb is
thread_local + libwebp per-instance so concurrent decodes are safe, the
in-flight slot is never leaked, and growth is bounded/cleared on navigate.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:55:11 -05:00
DanSandClaude Opus 4.8 a7c50ff19b perf(image-picker): decode hover animations off-thread (no UI hang)
Hovering an animated GIF/WebP previously decoded ALL frames + uploaded every
texture synchronously on the UI thread — a big/long animation froze the UI for
that first hover (stb's GIF decode is monolithic).

Now the decode runs on a detached background worker (bounded to 2 concurrent),
and frames are uploaded to GPU textures a few per UI frame; the still thumbnail
keeps showing until the sequence is ready, then it animates. A shared_ptr job
keeps the worker's result alive if the thumbnail is destroyed mid-decode (e.g.
navigating away), so nothing blocks. Only images the cheap badge-probe already
flagged as animated ever spawn a worker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:50:27 -05:00
DanSandClaude Opus 4.8 e7f2d2e3c7 feat(image-picker): animated-image badge on GIF/WebP thumbnails
Animated thumbnails now show a small play-arrow badge (bottom-right) so users can
spot which images move before hovering; it's hidden while the image plays on
hover.

Detection is cheap — a new util::IsAnimatedImageFile probes without a full
decode: animated WebP via WebPGetFeatures.has_animation, and a multi-frame GIF
via a lightweight image-descriptor block walk (stops at the 2nd frame). The
picker only probes .gif/.webp thumbnails and caches the result on the Thumb.

Verified: animated GIF + animated WebP report animated; still GIF/WebP/PNG do not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 11:44:57 -05:00
DanSandClaude Opus 4.8 fd0cab41f0 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>
2026-07-15 11:22:57 -05:00
DanSandClaude Opus 4.8 923d086092 feat(images): WebP decode via libwebp + more stb formats
Broaden avatar/image support:

- Add libwebp (FetchContent, static, decode-only) so WebP loads. Built from
  source for Linux / mingw-Windows / macOS-osxcross identically — the cross
  sysroots have no webp, so vendoring from source is the one portable path;
  encode/tool builds are disabled to avoid pulling in libpng/zlib. Linked as
  webp + webpdemux (the latter for animated WebP, wired next).
- texture_loader routes all decode through DecodeImageRGBA: sniffs the RIFF/
  WEBP header and uses libwebp (WebPDecodeRGBAInto into a free()-able buffer),
  else stb — so every existing caller (avatars, QR, thumbnails) gains WebP for
  free with no allocator mismatch.
- Enable stb's TGA / PSD / PNM / PIC decoders and add .webp/.tga/.psd/.pnm/
  .ppm/.pgm/.pic to the image-picker + avatar-library extension lists.

Verified: libwebp builds static and a real .webp decodes to correct RGBA.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 10:57:01 -05:00
DanSandClaude Opus 4.8 e1df5ea798 feat(image-picker): thumbnails fill width at 6 per row
Fix the thumbnail grid to a constant 6 columns whose square cells scale to the
available width, instead of fixed 96px cells that left dead space on the right.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:43:23 -05:00
DanSandClaude Opus 4.8 a53b13b6b6 feat(image-picker): smooth scroll, inset scrollbar, 2-column folder grid
- Smooth (lerped) wheel scrolling via ApplySmoothScroll on the list, matching
  the app's other modal lists.
- The list is now a bordered/rounded outer frame whose 6px padding insets the
  scrollbar so it clears the card's rounded corners; the inner scroll child is
  transparent (the frame draws the single background — no more box-in-a-box
  from the ChildBg being left on the stack across both BeginChild calls).
- Folders render as a 2-column grid of thin rounded rectangles (folder icon +
  name) instead of full-width rows, so more folders are visible at a glance.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:31:48 -05:00
DanSandClaude Opus 4.8 bc3c6037fc fix(image-picker): inset content from rounded card + center footer
- Wrap the picker body in a padded inner child so the filled directory list and
  thumbnail grid keep a clear margin from the card's rounded corners instead of
  running edge-to-edge past them.
- Center the Use image / Cancel buttons and drop the stray separator line above
  them (the faint artifact at the footer's left edge).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 09:09:18 -05:00
DanSandClaude Opus 4.8 2d4ba89c00 feat(contacts): revamp edit dialog with live preview + avatar picker
Rebuild the add/edit contact dialog on the portfolio-editor design language:

- A live preview card at the top shows the contact exactly as it renders in the
  list (avatar + name + address), updating as you type and pick an avatar.
- An avatar picker (Badge / Icon / Image segmented control) lets you keep the
  default Z/T type badge, choose a Material wallet icon from a searchable grid,
  or set a custom image. The picker area is fixed-height so the modal doesn't
  jump when switching modes.
- Custom images go through a new in-app ImagePicker (image_picker.h): a
  Material overlay that browses the filesystem starting at the user's Pictures
  folder, shows a thumbnail grid (decoded to raw pixels, box-downscaled to a
  small texture, cached per directory and freed on navigate/close, budgeted a
  few decodes per frame so large folders don't hitch), and returns the chosen
  path. The chosen image is copied into <config>/contact-avatars/ (named by an
  FNV hash of the source path, so re-picking is idempotent) and stored as
  "img:<path>". Like FolderPicker, it takes over the modal surface while open.
- Paste is now available on both add and edit; buttons are content-sized.

Adds three sweep surfaces (contacts-edit-{icon,badge,image}) that open the
dialog on a seeded contact in each avatar mode, plus i18n keys (+ 8-language
translations; reworded two zh/ja strings to stay within the existing CJK
subset, so no font rebuild).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 07:10:00 -05:00