Back to Blog

Display & Scale Images in egui — Rust GUI #17

Sandy LaneSandy Lane

Video: Display & Scale Images in egui — Rust GUI #17 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

egui::Image::new(source) plus the egui_extras image loader. Embed PNGs at compile time, render them with custom size and rounding.

Today's widget is images. egui can display PNG, JPEG, GIF, WebP, and a few others — but it needs a loader to decode the bytes, and the loader lives in the egui_extras crate. Plug it in once, and any egui::Image::new(source) call renders the image to your UI.

We build a small gallery: a sidebar with four thumbnails, a central preview pane with a zoom slider. Click a thumbnail, see it large.

What we are building

Four images embedded at compile time (include_image!). A left sidebar with the four thumbnails clickable. A central panel showing the selected image, with a zoom slider in the top panel. Code split across app.rs, sidebar.rs, and preview.rs — the multi-file project pattern you started seeing in Episode 3.

Setting up the loader

fn main() -> eframe::Result {
  let options = eframe::NativeOptions { /* ... */ };
  eframe::run_native(
    "Image Gallery",
    options,
    Box::new(|cc| {
      egui_extras::install_image_loaders(&cc.egui_ctx);
      Ok(Box::new(app::MyApp::default()))
    }),
  )
}

The key new line is egui_extras::install_image_loaders(&cc.egui_ctx). This registers PNG, JPEG, and other decoders with the egui context. Without it, egui::Image::new(source) would compile but show a "No image loader for this URI" error at runtime.

This is the one-time setup cost of using images. After it, every Image::new call works.

You also need egui_extras = { version = "0.31", features = ["all_loaders"] } in your Cargo.toml. The all_loaders feature pulls in support for PNG, JPEG, WebP, etc.

include_image!

let sources = [
  egui::include_image!("../assets/sunset.png"),
  egui::include_image!("../assets/forest.png"),
  egui::include_image!("../assets/ocean.png"),
  egui::include_image!("../assets/mountain.png"),
];

include_image! is a macro that embeds the bytes of an image file into the compiled binary. The path is relative to the source file. At runtime, no file I/O happens — the image data is part of the binary.

This is great for icons, app illustrations, fixed assets — anything that should ship with the app and never change. It is wrong for user-uploaded images, which you load with Image::from_uri("file:///path/to/image.png") or by feeding bytes through a LoadOps.

The macro returns egui::ImageSource, the type accepted by egui::Image::new.

egui::Image

ui.add(
  egui::Image::new(sources[app.selected].clone())
    .fit_to_exact_size(size)
    .corner_radius(8.0),
);

Image::new(source) builds an image widget. Builder methods control display:

  • .fit_to_exact_size(size) — resize to a specific Vec2.
  • .fit_to_original_size(scale) — render at the original pixel size, scaled by a factor.
  • .fit_to_max_size(max) — fit within a maximum, preserving aspect ratio.
  • .max_width(px) / .max_height(px) — width or height caps.
  • .corner_radius(px) — round the image's corners.
  • .tint(Color32) — apply a tint multiplied with the image.
  • .sense(Sense::click()) — make the image clickable; the returned Response gives you .clicked().

The image source can be cloned cheaply — ImageSource is a small enum referring to the embedded bytes, not a pixel buffer. Cloning per frame is fine.

Clickable thumbnails

let response = ui.add(
  egui::Image::new(sources[i].clone())
    .max_width(160.0)
    .corner_radius(4.0)
    .sense(egui::Sense::click()),
);
if response.clicked() {
  app.selected = i;
}

By default, Image does not respond to clicks — it is a display widget. .sense(egui::Sense::click()) enables click handling, and the returned Response gives the .clicked() method we know from buttons.

Below the thumbnail we render the image's name, with a strong() style for the currently selected one:

if app.selected == i {
  ui.strong(names[i]);
} else {
  ui.label(names[i]);
}

Visual feedback that costs zero state: the styling is read from the same selected field every frame.

Multi-file project structure

src/
├── main.rs       # entry point, sets up loaders
├── app.rs        # app state and the update method
├── sidebar.rs    # the thumbnail gallery
└── preview.rs    # the zoomable preview

main.rs declares all three modules with mod app;, mod sidebar;, mod preview;. sidebar.rs and preview.rs each export a pub fn show(app: &mut MyApp, ctx: &Context, ...) function called from app.rs.

This is the right pattern for any egui project past 200 lines. The app struct and its update method stay short; the sub-functions handle individual panels.

Zoom

ui.add(egui::Slider::new(&mut app.zoom, 0.5..=3.0));
// ...
let size = egui::vec2(320.0 * app.zoom, 240.0 * app.zoom);
ui.add(egui::Image::new(sources[app.selected].clone()).fit_to_exact_size(size));

The zoom slider sets a multiplier; the preview's display size is the base size times the multiplier. Drag the slider — the image grows or shrinks. For very large zoom values, the preview might overflow the central panel — wrap it in ScrollArea::both() so the user can pan around.

Running it

cargo run. The window opens with four thumbnails in the sidebar and the first image (Sunset) in the preview. Click a different thumbnail — the preview switches. Drag the Zoom slider — the preview resizes. Drag the slider all the way to 3.0 — the image is large enough to need scrollbars (the ScrollArea::both() in preview.rs handles that).

Other image sources

Beyond include_image!, you can:

  • Image::from_uri("file:///absolute/path.png") — load from disk at runtime.
  • Image::from_uri("https://example.com/image.png") — load from the network (requires the http feature on egui_extras).
  • Image::from_bytes("name.png", bytes) — pass raw bytes you have in memory.

For dynamic content (user uploads, generated images, network downloads) those forms are essential. For static assets, include_image! keeps things simple.

Common mistakes

Forgetting install_image_loaders. Compiles fine; runtime warning about no loader. Always install in main or in the CreationContext callback.

Forgetting the egui_extras features. Default features do not enable image loaders. Add features = ["all_loaders"] (or specific loaders like ["image"]) to your Cargo.toml.

Calling clone() on raw image bytes. ImageSource::clone is cheap (it clones a small enum). Cloning every frame is fine.

Including very large images with include_image!. They get baked into the binary. Multi-megabyte images bloat your executable. Use file URIs or remote loading for large content.

What's next

Next episode: tooltips. .on_hover_text("...") and .on_hover_ui(|ui| ...) to attach explanatory text to any widget. Small but crucial UX detail — every button in a real app benefits from a tooltip.

Recap

egui::Image::new(source) for image display, with egui_extras::install_image_loaders(&ctx) registered once in main. include_image! to embed at compile time, from_uri for runtime loading. Builder methods like .fit_to_exact_size, .corner_radius, .sense(Sense::click()) for layout and interactivity. Split projects into main.rs plus per-panel modules.

Next episode: tooltips. See you in the next one.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.