marten/prototypes/SPIKE_COMPARISON.md

7.1 KiB
Raw Blame History

Spike Comparison: iced vs egui

Both prototypes implement the same MVP slice so we can compare apples-to-apples:

  • Open a folder of images via dialog (O key or button)
  • Display current image, fit-to-window
  • Scroll wheel = prev/next photo (ristretto-style, natural scrolling)
  • Arrow keys + hjkl navigate
  • Q to quit
  • Dark theme

Neither spike implements: thumbnail bar, zoom/pan, right-click menu, fullscreen, EXIF panel. Those come after we pick a winner.


How to run

# iced spike
cd /home/z/my-project/image-viewer/prototypes/iced-viewer
cargo run --release

# egui spike
cd /home/z/my-project/image-viewer/prototypes/egui-viewer
cargo run --release

In each: press O, pick a folder with some images, then scroll wheel / arrow keys to navigate.


Comparison criteria

Score each on a 15 scale (5 = best). Fill in after testing.

1. Visual polish (the ristretto+ bar)

  • Does the dark theme look refined out of the box, or does it look like a "default toolkit demo"?
  • Is the toolbar thin and unobtrusive?
  • Does the image area background blend cleanly with the chrome?
  • Score: iced __ / 5 · egui __ / 5

2. Scroll-wheel feel

  • Is the scroll responsive, or is there noticeable lag?
  • Does fast scrolling skip photos or queue them up?
  • Does the cursor need to be over the image, or does any scroll anywhere navigate?
  • Score: iced __ / 5 · egui __ / 5

3. Image rendering quality

  • Does the image render crisp at fit-to-window scale?
  • Are colors correct (no premultiplied-alpha artifacts on transparent PNGs)?
  • Does animated GIF play (it shouldn't in spike, but note if it does)?
  • Score: iced __ / 5 · egui __ / 5

4. Code ergonomics

  • How easy was it to express the ristretto layout? (Lines of code, mental overhead)
  • How clean is the event loop? (declarative Msg vs immediate-mode input polling)
  • Async image loading: which approach felt more natural?
  • Score: iced __ / 5 · egui __ / 5

5. Thumbnail bar viability (look ahead)

  • Can we plausibly build a horizontal scrollable thumbnail strip with custom styling?
  • Will it be easy to make thumbnails load lazily and replace placeholder textures?
  • Score: iced __ / 5 · egui __ / 5

6. Right-click menu viability (look ahead)

  • How easy is a custom context menu with icons, separators, submenus?
  • Score: iced __ / 5 · egui __ / 5

7. Fullscreen + auto-hide controls (look ahead)

  • Can we cleanly toggle chrome visibility at runtime?
  • Score: iced __ / 5 · egui __ / 5

8. Build time / binary size

  • iced release binary: ~23 MB
  • egui release binary: __ MB (fill in)
  • Cold build time (cargo clean && time cargo build --release):
    • iced: __ s
    • egui: __ s

Decision matrix

Criterion Weight iced egui
Visual polish 3
Scroll feel 3
Image quality 2
Code ergonomics 2
Thumb bar viability 3
Right-click menu 2
Fullscreen toggle 1
Build/binary 1

Weighted total: iced __ · egui __


Verdict

Filled in after runtime testing on Arch Linux (2026-08-02).

Winner: iced

Why:

  • iced ran smoothly out of the box; egui loaded but exhibited visible runtime issues (rendering/input quirks) that would have cost debugging time before any real feature work could begin.
  • iced's Theme::Dark was closer to the ristretto+ target aesthetic without manual overrides.
  • The declarative Message enum + Task::perform async story felt like a better fit for an app with this much interaction surface (scroll, zoom, pan, context menu, fullscreen, thumbnail clicks, configurable keymap).
  • egui's lack of a built-in async story was already forcing us into std::thread + JoinHandle polling for the file dialog — that pattern does not scale to a lazy-loaded thumbnail bar.

Trade-offs we accept:

  • Larger binary (23 MB vs 15.5 MB). Acceptable for a desktop app; we'll trim later with strip + lto = "fat" if size becomes an issue.
  • No built-in context-menu widget — we'll roll our own with an overlay layer. This is actually a feature: we wanted full styling control for the gpicview-inspired menu anyway.
  • Fullscreen toggling will go through iced's window subsystem rather than egui's trivial ViewportBuilder. Slightly more code, same end result.

What we lose by not picking egui:

  • Response::context_menu() for free right-click menus (we'll write our own — see above).
  • ViewportBuilder one-liner for fullscreen (we'll use iced's window commands).
  • Immediate-mode layout simplicity (declarative state is a net win for this app's complexity).
  • ~7 MB of binary size.

The archived egui spike lives at prototypes/egui-viewer-archived/ with a README.md explaining the call. We keep it as a reference and as a restart path if iced turns out to be the wrong choice after MVP.


Notes from initial code-side impressions (pre-runtime)

These are observations from writing both spikes, before any runtime testing.

iced 0.13 — code-side notes

  • Pros
    • Declarative Message enum makes the state machine explicit and easy to reason about.
    • Built-in Task::perform for async file loading is clean and integrated with the runtime.
    • Theme::Dark is one-liner; refinement via container::Style closures is straightforward.
    • image::Handle::from_bytes lets us defer decoding to the runtime — no manual texture upload.
    • Subscription + iced::event::listen() is a clean way to capture global input.
  • Cons
    • 0.13 is a recent release; some docs still show 0.12 patterns. We already hit one breaking change (center_x/align_x).
    • The image widget doesn't expose a "fit mode + actual displayed size" API — we'd need to compute scaling ourselves for zoom/pan.
    • No built-in context-menu widget; we'll roll our own with overlays.
    • Custom thumbnail bar with lazy texture loading will require careful Handle management.

egui 0.29 — code-side notes

  • Pros
    • Immediate mode makes the layout trivial — TopBottomPanel + CentralPanel is 5 lines.
    • ColorImage + TextureHandle is a textbook path; lazy thumbnail loading is natural (just stash handles in a Vec).
    • Context::request_repaint_after gives us precise repaint control (good for animated GIFs later).
    • Built-in Response::context_menu() gives us right-click menus for free.
    • ViewportBuilder makes fullscreen toggling trivial.
  • Cons
    • No async story — we spawn std::thread and poll JoinHandle::is_finished() ourselves. This will get messy for a real thumbnail bar.
    • egui::Color32 is RGBA8 — HDR/16-bit workflows will need tone-mapping in our code.
    • Default dark theme is bluish; we had to override the panel fill manually to get the ristretto-feel dark.
    • Immediate mode means we re-emit the entire UI every frame; for a static viewer this is mostly fine but burns CPU on idle.

Action after decision

Once we pick:

  1. Move winner to image-viewer/src/ (single binary crate as agreed).
  2. Archive loser under image-viewer/prototypes/<loser>-archived/ with a README.md saying why.
  3. Add a DECISION.md at repo root summarizing this doc.
  4. Begin MVP build: modular src/{image,nav,ui,config}/ structure.