Images
Guido supports displaying both raster images (PNG, JPEG, GIF, WebP) and SVG vector graphics. Images are rendered as GPU textures and compose seamlessly with container transforms.
Basic Usage
The image() function creates an image widget from a file path:
extern crate guido;
fn main() {
use guido::prelude::*;
// Load a PNG image
container()
.width(32.0)
.height(32.0)
.child(image("./icon.png"));
// Load an SVG (auto-detected by extension)
container()
.width(100.0)
.height(100.0)
.child(image("./logo.svg"))
;
}
Image Sources
You can load images from different sources using ImageSource:
extern crate guido;
use guido::prelude::*;
fn main() {
let svg_string = String::new();
let image_bytes: &[u8] = &[];
let rgba_bytes: Vec<u8> = Vec::new();
// From file path (raster)
ImageSource::Path("./photo.jpg".into());
// From memory (raster)
ImageSource::Bytes(image_bytes.into());
// From file path (SVG)
ImageSource::SvgPath("./icon.svg".into());
// From memory (SVG)
ImageSource::SvgBytes(svg_string.as_bytes().into());
// Raw pre-decoded RGBA8 pixels (width * height * 4 bytes, row-major)
ImageSource::Rgba { width: 22, height: 22, pixels: rgba_bytes.into() }
;
}
When using a string path with image(), the file extension determines the type automatically: .svg files use SVG rendering, all others use raster decoding.
ImageSource::Rgba skips decoding entirely — use it for pixel data that never existed in an encoded format, such as tray icon pixmaps or album art received over D-Bus. Its pixels have straight alpha, as a decoder gives them, not premultiplied.
Loading in the Background
A raster Path or Bytes source is decoded on a background thread, not in the
frame that first shows it. A large wallpaper takes a noticeable fraction of a
second to decode, and a frame that waited for it would put nothing on screen
until then. So the first frame goes out without the picture: the box is laid out
at the image’s size, read from the file’s header, and nothing is drawn in it.
When the decode finishes the image is drawn on the next frame, with nothing from
your application needed to prompt it. Two images showing the same source share
one decode.
An SVG is rasterized at the size it is shown. An icon-sized one is cheap enough to draw in the frame that shows it, so it appears at once. A large one — anything past 128 × 128 pixels once HiDPI and the 2× quality are counted — is rasterized on the same background thread and appears on a later frame the same way. When its box changes size it needs a new raster, and until that one is ready the previous one is drawn, stretched, so a large SVG in a resize animation never blinks out.
The image does not fade itself in. It tells you when it is ready, as a signal,
and a fade is then an opacity with a transition on the container around it:
extern crate guido;
use guido::prelude::*;
fn main() {
let wallpaper = image("./wallpaper.png").content_fit(ContentFit::Cover);
let ready = wallpaper.ready();
container()
.width(fill())
.height(fill())
.opacity((move || if ready.get() { 1.0 } else { 0.0 }).transition(200.0))
.child(wallpaper)
;
}
ready() follows the source: when a reactive source changes, it is false until
the new one is decoded. A source that failed to decode is never ready, and says
why once in the log. An SVG is ready from the first frame that draws it: an
icon-sized one is drawn, and usually ready, from the start, and a large one once
its first raster lands. ImageSource::Rgba is ready from the start — the way to
have a raster image in the very first frame, if you decoded it yourself.
Sizing
The box comes from the enclosing container, like every other size in guido; the image decides only how its pixels land inside it.
extern crate guido;
use guido::prelude::*;
fn main() {
// A 32x32 box
container().width(32.0).height(32.0).child(image("./icon.png"));
// Width fixed, height from the aspect ratio (the default fit is Contain)
container().width(200.0).child(image("./banner.png"));
// No box at all - the image reports its intrinsic size
image("./icon.png")
;
}
Content Fit Modes
The content_fit() method controls how the image maps into the box it is
given:
| Mode | Description |
|---|---|
ContentFit::Contain | Fit within the box, preserving aspect ratio (default) |
ContentFit::Cover | Fill the box, cropping, preserving aspect ratio |
ContentFit::Fill | Stretch to fill exactly, ignoring aspect ratio |
ContentFit::None | Intrinsic size, ignoring the box |
Cover and Fill take all the room the container offers; they differ in how
the pixels land. Contain takes only the largest rect of the image’s own
aspect ratio that fits, so the empty strip is left to the parent’s alignment
rather than painted.
Like every declared value, it takes a signal as readily as a value, so an image can change how it fills its box without being rebuilt:
extern crate guido;
use guido::prelude::*;
fn main() {
let cropped = create_signal(true);
image("./photo.jpg").content_fit(move || {
if cropped.get() {
ContentFit::Cover
} else {
ContentFit::Contain
}
})
;
}
extern crate guido;
use guido::prelude::*;
fn main() {
let wallpaper = "./wallpaper.jpg";
// Cover a 200x150 box, cropping what does not fit
container()
.width(200.0)
.height(150.0)
.child(image("./photo.jpg").content_fit(ContentFit::Cover));
// A wallpaper: cover the whole surface
container()
.width(fill())
.height(fill())
.child(image(wallpaper).content_fit(ContentFit::Cover))
;
}
Transform Composition
Images inherit transforms from parent containers, just like text:
extern crate guido;
use guido::prelude::*;
fn main() {
// Rotated image
container()
.rotate(15.0)
.child(
container()
.width(32.0)
.height(32.0)
.child(image("./badge.svg"))
);
// Scaled image
container()
.scale(1.5)
.child(
container()
.width(24.0)
.height(24.0)
.child(image("./icon.png"))
);
// Combined transforms
container()
.rotate(45.0)
.scale(2.0)
.child(image("./logo.svg"))
;
}
SVG Quality
SVGs are automatically rasterized at the appropriate scale for crisp rendering:
- HiDPI displays: SVGs render at the display scale factor
- Transforms: When scaled via container transforms, SVGs re-rasterize at the higher resolution
- Quality: A 2x supersampling multiplier ensures smooth edges
- Content fit: The raster is made at the size the fit draws it at — the box’s
width and height for
ContentFit::Fill, and forContentFit::Coveronly the part the box shows, at the scale that covers it — so a stretched or covering SVG is as sharp as a contained one, and a covering one costs no more than its box
This means SVGs stay crisp regardless of how they’re scaled or transformed.
In-Memory SVGs
For dynamically generated or embedded SVGs:
extern crate guido;
use guido::prelude::*;
fn main() {
let svg_data = r##"
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<circle cx="50" cy="50" r="40" fill="#4f46e5" />
</svg>
"##;
container()
.width(48.0)
.height(48.0)
.child(image(ImageSource::SvgBytes(svg_data.as_bytes().into())))
;
}
An Image carries no box of its own: the container it sits in is what gives it
one, exactly as with a text. content_fit then decides how the picture uses
that box.
Tinting
tint draws every pixel of an image in one colour, keeping its alpha. It is
how a monochrome icon takes the colour of the text beside it, a hover, or a
theme:
extern crate guido;
use guido::prelude::*;
fn main() {
let hovered = create_signal(false);
container()
.width(16.0)
.height(16.0)
.on_hover(move |h| hovered.set(h))
.child(image("./battery.svg").tint(move || {
if hovered.get() { Color::WHITE } else { Color::rgb(0.6, 0.6, 0.6) }
}))
;
}
The colour is applied while the image is drawn, from the texture it already
has, so changing it costs a repaint rather than rasterizing the SVG again. The
tint’s alpha fades the image as opacity would.
An icon inside a button rarely needs a signal of its own for this. An image takes state layers as a text does, and they follow the control it sits in, so the icon brightens and dims with its label:
extern crate guido;
use guido::prelude::*;
fn main() {
let weak = Color::rgb(0.6, 0.6, 0.6);
let dim = Color::rgb(0.3, 0.3, 0.3);
let busy = create_signal(false);
container()
.layout(Flex::row().spacing(8.0))
.enabled(move || !busy.get())
.on_click(|| {})
.child(
container().width(16.0).height(16.0).child(
image("./power.svg")
.tint(weak.transition(150.0))
.when_hovered(|s| s.tint(Color::WHITE))
.when_disabled(|s| s.tint(dim)),
),
)
.child(
text("Power off")
.color(weak.transition(150.0))
.when_hovered(|s| s.color(Color::WHITE))
.when_disabled(|s| s.color(dim)),
)
;
}
A transition declared on the tint eases the change between states; without one the tint switches.
Reactive Images
Image sources can be reactive, allowing dynamic image changes:
extern crate guido;
use guido::prelude::*;
fn main() {
let icon_source = create_signal(ImageSource::Path("./play.png".into()));
// The image updates when the signal changes
container()
.width(32.0)
.height(32.0)
.child(image(icon_source));
// Change the image on click
container()
.on_click(move || {
icon_source.set(ImageSource::Path("./pause.png".into()));
})
.child(image(icon_source))
;
}
Supported Formats
Raster Formats
- PNG
- JPEG
- GIF
- WebP
A photo is shown the way its EXIF orientation says, as a browser shows it. A phone often stores a portrait photo’s pixels landscape and records the turn in the metadata; such a photo is laid out portrait and drawn upright. JPEG, PNG and WebP carry the orientation; a GIF has none and is shown as stored.
Vector Formats
- SVG
Example
Here’s a complete example showing various image features:
extern crate guido;
use guido::prelude::*;
fn main() {
App::new().run(|app| {
let svg_icon = r##"
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" fill="#3b82f6"/>
</svg>
"##;
let view = container()
.padding(16.0)
.layout(Flex::row().spacing(16.0))
.child(
// PNG image
container()
.width(48.0)
.height(48.0)
.child(image("./logo.png"))
)
.child(
// SVG from memory
container()
.width(32.0)
.height(32.0)
.child(image(ImageSource::SvgBytes(svg_icon.as_bytes().into())))
)
.child(
// Rotated image
container()
.rotate(15.0)
.child(
container()
.width(24.0)
.height(24.0)
.child(image("./icon.svg"))
)
);
app.add_surface(
SurfaceConfig::new()
.height(80)
.background_color(Color::rgb(0.1, 0.1, 0.15)),
move || view,
);
});
}
Performance Notes
- Raster files and bytes are decoded off the frame, once per source, and the decoded pixels are dropped once they are uploaded: after that the GPU texture is the image. If the texture is later evicted, the image is decoded again the next time it is drawn
- Images are cached as GPU textures. Small ones — up to 256 pixels a side once rasterized — share atlas pages, so a screen full of icons is a few draw calls, not one per icon
- The cache’s budget is 100 MB of images (about width × height × 4 bytes each) and it evicts
the least recently drawn past that, never one drawn in the last second. An application
with other needs sets its own budget with
App::image_cache_budget. The atlas pages the small images share are 4 MB each and are not counted: a page is given back once eviction has emptied it, so the GPU memory held can run a few pages past the budget - SVGs are re-rasterized when their display size or scale changes — in the frame for an icon, off it for a large one, which draws the previous raster stretched until the new one lands
- Texture uploads happen once per unique image/scale combination