Display & Scale Images in egui — Rust GUI #17
Video: Display & Scale Images in egui — Rust GUI #17 by Taught by Celeste AI - AI Coding Coach
egui::Image::new(source)plus theegui_extrasimage 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 specificVec2..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 returnedResponsegives 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 thehttpfeature onegui_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.