Image

GPUI’s img() draws an image, and svg() draws a single-color icon. uni-kit re-exports both from gpui_kit. This page shows the patterns an application uses most.

Install

#Start with a working image

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// A source paints the frame; no source keeps the frame and centres the
// placeholder, so a missing asset never leaves an empty hole in a layout.
// The third frame is fetched from a URL at startup: it shows the same
// placeholder while the fetch is in flight and after one fails, which is
// the state a reader offline sees.
let working = row![
    labelled(
        "With a source",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Contain)
            .height(Length::Fixed(140.0)),
    ),
    labelled(
        "Without one",
        ImageView::new()
            .height(Length::Fixed(140.0))
            .placeholder(placeholder_icon()),
    ),
    labelled(
        &app.online.caption(),
        match app.online.source() {
            Some(source) => ImageView::new()
                .source(source)
                .fit(ImageFit::Contain)
                .height(Length::Fixed(140.0)),
            None => ImageView::new()
                .fit(ImageFit::Contain)
                .height(Length::Fixed(140.0))
                .placeholder(placeholder_icon()),
        },
    ),
]
.spacing(theme::SPACING_LG);

This complete native src/main.rs uses an icon already bundled by uni-kit, so it needs no extra asset file. Add gpui-kit = "{{gpui_kit_version}}" to Cargo.toml. The same asset is shown as a color-preserving image and as a monochrome SVG.

This complete native src/main.rs uses an icon already bundled by uni-kit, so it needs no extra asset file. Add gpui-kit = "{{gpui_kit_version}}" to Cargo.toml. The same asset is shown as a color-preserving image and as a theme-colored monochrome SVG; the difference is explained below.

use gpui_kit::*;
use gpui_kit::assets::Assets;

struct ImageExample;

impl Render for ImageExample {
    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
        div()
            .flex()
            .gap_4()
            .child(
                img("icons/inbox.svg")
                    .id("inbox-image")
                    .size(px(64.))
                    .object_fit(ObjectFit::Contain)
                    .with_loading(|| div().child("Loading image...").into_any_element())
                    .with_fallback(|| div().child("Image unavailable").into_any_element()),
            )
            .child(svg().path("icons/inbox.svg").size(px(64.)))
    }
}

fn main() {
    gpui_kit::application().with_assets(Assets).run(|cx| {
        gpui_kit::init(cx);
        gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
            cx.new(|_| ImageExample)
        })
        .expect("Failed to open window");
    });
}
// Installed in ui/components. That directory is on the Slint include path, so import by file name:
import { ImageFit, ImageView } from "image.slint";

with_assets(Assets) registers the default component icons; register your own AssetSource to embed your images (see Icon). A string such as "images/cover.png" is an asset key, a Path reads a file, and a URL is fetched through the App’s HttpClient.

#Common patterns

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// `fit` decides how a source fills its frame: cover crops the overflow,
// contain letterboxes it over the frame's fill, fill stretches it. The
// source is a real square picture, so a wide frame makes the three
// visibly different — a circle stays a circle, gains bars, or becomes an
// ellipse.
let fits = row![
    labelled(
        "cover — crops the overflow",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Cover)
            .height(Length::Fixed(120.0)),
    ),
    labelled(
        "contain — letterboxes",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Contain)
            .height(Length::Fixed(120.0)),
    ),
    labelled(
        "fill — stretches",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Fill)
            .height(Length::Fixed(120.0)),
    ),
]
.spacing(theme::SPACING_MD);

// `radius` shapes the frame, and the source is clipped to the same corner.
// A rounded square and a circle come from the same component.
let radii = row![
    labelled(
        "radius 3",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Cover)
            .radius(theme::RADIUS_SM)
            .width(Length::Fixed(72.0))
            .height(Length::Fixed(72.0)),
    ),
    labelled(
        "radius 8",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Cover)
            .radius(theme::RADIUS_LG)
            .width(Length::Fixed(72.0))
            .height(Length::Fixed(72.0)),
    ),
    labelled(
        "a full circle",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Cover)
            .radius(36.0)
            .width(Length::Fixed(72.0))
            .height(Length::Fixed(72.0)),
    ),
    labelled(
        "the default",
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Cover)
            .width(Length::Fixed(72.0))
            .height(Length::Fixed(72.0)),
    ),
]
.spacing(theme::SPACING_MD)
.align_y(Alignment::Start);

let patterns = column![fits, radii].spacing(theme::SPACING_LG);

A thumbnail that fills a fixed box and crops its edges:

img("images/cover.png")
    .w(px(320.))
    .h(px(180.))
    .object_fit(ObjectFit::Cover)
VerticalLayout {
    spacing: 16px;
    // `fit` decides how a source fills its frame.
    HorizontalLayout {
        spacing: 12px;
        VerticalLayout {
            horizontal-stretch: 1;
            spacing: 6px;
            ImageView {
                height: 120px;
                fit: ImageFit.cover;
                source: @image-url("../icons/chart-column.svg");
            }
            Text {
                text: "cover — crops the overflow";
                color: Theme.muted-foreground;
                font-size: Theme.font-size-xs;
            }
        }
        VerticalLayout {
            horizontal-stretch: 1;
            spacing: 6px;
            ImageView {
                height: 120px;
                fit: ImageFit.contain;
                source: @image-url("../icons/chart-column.svg");
            }
            Text {
                text: "contain — letterboxes";
                color: Theme.muted-foreground;
                font-size: Theme.font-size-xs;
            }
        }
        VerticalLayout {
            horizontal-stretch: 1;
            spacing: 6px;
            ImageView {
                height: 120px;
                fit: ImageFit.fill;
                source: @image-url("../icons/chart-column.svg");
            }
            Text {
                text: "fill — stretches";
                color: Theme.muted-foreground;
                font-size: Theme.font-size-xs;
            }
        }
    }
    // `radius` shapes the frame. A rounded square and a circle both come
    // from the same component.
    HorizontalLayout {
        spacing: 12px;
        alignment: start;
        ImageView {
            width: 72px;
            height: 72px;
            radius: Theme.radius-sm;
            fit: ImageFit.cover;
            source: @image-url("../icons/user.svg");
        }
        ImageView {
            width: 72px;
            height: 72px;
            radius: Theme.radius-lg;
            fit: ImageFit.cover;
            source: @image-url("../icons/user.svg");
        }
        ImageView {
            width: 72px;
            height: 72px;
            radius: 36px;
            fit: ImageFit.cover;
            source: @image-url("../icons/user.svg");
        }
        Text {
            text: "radius: 3px, 8px, and a full circle";
            color: Theme.muted-foreground;
            font-size: Theme.font-size-xs;
            vertical-alignment: center;
        }
    }
}

A banner that follows the parent width and reserves its height before the image arrives:

img("images/banner.webp")
    .w_full()
    .aspect_ratio(16. / 9.)
    .object_fit(ObjectFit::Cover)
SlintNo Slint version of this example yet.

A remote image with loading and failure states. The .id(...) is required for the loading state to appear:

img("https://example.com/avatar.png")
    .id("avatar")
    .size(px(48.))
    .rounded_full()
    .with_loading(|| div().child("Loading image...").into_any_element())
    .with_fallback(|| div().child("Image unavailable").into_any_element())
SlintNo Slint version of this example yet.

Use ObjectFit::Contain (the default) for logos and diagrams that must stay fully visible. To keep a multicolor SVG’s colors, draw it with img(); for an icon that follows the theme, use Icon.

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// The selected cell is the frame's job, not the image's: a ring around the
// `ImageView` reads as selection without recolouring the picture. Pressing a
// cell moves the ring, which is the one interaction this demo drives.
let mut gallery = row![].spacing(theme::SPACING_SM);
for index in 0..6 {
    gallery = gallery.push(thumbnail(app.local.clone(), index, app.selected == index));
}

A thumbnail that changes the main image is a control. Give it a real Button so keyboard activation and an accessible name work, and keep the selected index in the view’s retained state. This complete src/main.rs uses three icons already shipped with uni-kit; replace the source array with your own registered image keys for a photo gallery. Run it with the same gpui-kit = "{{gpui_kit_version}}" dependency as the first example.

use gpui_kit::*;
use gpui_kit::assets::Assets;
use gpui_kit::base::Selectable;
use gpui_kit::component::button::Button;

const PICTURES: [(&str, &str); 3] = [
    ("icons/inbox.svg", "Inbox"),
    ("icons/book-open.svg", "Book"),
    ("icons/gallery-vertical-end.svg", "Gallery"),
];

struct Gallery {
    selected: usize,
}

impl Render for Gallery {
    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let (source, name) = PICTURES[self.selected];

        div()
            .flex()
            .flex_col()
            .gap_3()
            .p_4()
            .child(format!("Selected: {name}"))
            .child(
                img(source)
                    .id("gallery-main")
                    .w(px(360.))
                    .h(px(240.))
                    .object_fit(ObjectFit::Contain)
                    .with_fallback(|| div().child("Image unavailable").into_any_element()),
            )
            .child(
                div().flex().gap_2().children(
                    PICTURES.iter().enumerate().map(|(index, (source, name))| {
                        Button::new(format!("gallery-thumbnail-{index}"))
                            .accessibility_label(format!("Show {name}"))
                            .selected(index == self.selected)
                            .child(
                                img(*source)
                                    .size(px(40.))
                                    .object_fit(ObjectFit::Contain),
                            )
                            .on_click(cx.listener(move |this, _, _, cx| {
                                this.selected = index;
                                cx.notify();
                            }))
                    }),
                ),
            )
    }
}

fn main() {
    gpui_kit::application().with_assets(Assets).run(|cx| {
        gpui_kit::init(cx);
        gpui_kit::open_window(WindowOptions::default(), cx, |_, cx| {
            cx.new(|_| Gallery { selected: 0 })
        })
        .expect("Failed to open window");
    });
}
HorizontalLayout {
    spacing: 8px;
    Rectangle {
        horizontal-stretch: 1;
        height: 104px;
        border-radius: Theme.radius-lg;
        border-width: 1px;
        border-color: Theme.border;
        background: Theme.muted.with-alpha(0.3);
        ImageView {
            x: 8px;
            y: 8px;
            width: parent.width - 16px;
            height: parent.height - 16px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/image.svg");
        }
    }
    Rectangle {
        horizontal-stretch: 1;
        height: 104px;
        border-radius: Theme.radius-lg;
        border-width: 1px;
        border-color: Theme.border;
        background: Theme.muted.with-alpha(0.3);
        ImageView {
            x: 8px;
            y: 8px;
            width: parent.width - 16px;
            height: parent.height - 16px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/palette.svg");
        }
    }
    Rectangle {
        horizontal-stretch: 1;
        height: 104px;
        border-radius: Theme.radius-lg;
        border-width: 2px;
        border-color: Theme.primary;
        background: Theme.muted.with-alpha(0.3);
        ImageView {
            x: 8px;
            y: 8px;
            width: parent.width - 16px;
            height: parent.height - 16px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/globe.svg");
        }
    }
    Rectangle {
        horizontal-stretch: 1;
        height: 104px;
        border-radius: Theme.radius-lg;
        border-width: 1px;
        border-color: Theme.border;
        background: Theme.muted.with-alpha(0.3);
        ImageView {
            x: 8px;
            y: 8px;
            width: parent.width - 16px;
            height: parent.height - 16px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/github.svg");
        }
    }
    Rectangle {
        horizontal-stretch: 1;
        height: 104px;
        border-radius: Theme.radius-lg;
        border-width: 1px;
        border-color: Theme.border;
        background: Theme.muted.with-alpha(0.3);
        ImageView {
            x: 8px;
            y: 8px;
            width: parent.width - 16px;
            height: parent.height - 16px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/cpu.svg");
        }
    }
    Rectangle {
        horizontal-stretch: 1;
        height: 104px;
        border-radius: Theme.radius-lg;
        border-width: 1px;
        border-color: Theme.border;
        background: Theme.muted.with-alpha(0.3);
        ImageView {
            x: 8px;
            y: 8px;
            width: parent.width - 16px;
            height: parent.height - 16px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/database.svg");
        }
    }
}

The selected button has an explicit selected state, and the visible Selected: ... text reports the current choice. In a product gallery, use meaningful labels such as Show front view rather than a file name. If the collection can be empty, render an empty-state message before indexing it. If image sources can be removed or reordered, store a stable domain ID instead of an index and resolve it during render.

#Accessibility

GPUINo GPUI version of this example yet.
SlintNo Slint version of this example yet.
// An image that carries meaning needs its description in the text beside it;
// one that only decorates needs none.
let accessibility = column![
    row![
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Contain)
            .width(Length::Fixed(48.0))
            .height(Length::Fixed(48.0)),
        text("Meaningful: the caption carries what the picture shows.")
            .size(theme::FONT_SIZE_SM)
            .style(|theme: &Theme| text::Style {
                color: Some(Tokens::of(theme).foreground),
            }),
    ]
    .spacing(theme::SPACING_MD)
    .align_y(Alignment::Center),
    row![
        ImageView::new()
            .source(app.local.clone())
            .fit(ImageFit::Contain)
            .width(Length::Fixed(48.0))
            .height(Length::Fixed(48.0)),
        text("Decorative: the text already says it, so the picture adds nothing.")
            .size(theme::FONT_SIZE_SM)
            .style(|theme: &Theme| text::Style {
                color: Some(Tokens::of(theme).muted_foreground),
            }),
    ]
    .spacing(theme::SPACING_MD)
    .align_y(Alignment::Center),
]
.spacing(theme::SPACING_LG);
  • Pair an informative image with visible text or an accessible description in the surrounding UI. A file name is not a description. Decorative images need no narration.
  • Make an image that triggers a command a real control, such as a Button, so it has a name, focus, and keyboard activation.
  • Keep the loading and failure views in the same box as the image, so nearby content does not move.
GPUINo GPUI version of this example yet.
VerticalLayout {
    spacing: 16px;
    HorizontalLayout {
        spacing: 12px;
        alignment: center;
        ImageView {
            width: 48px;
            height: 48px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/shield.svg");
        }
        Text {
            horizontal-stretch: 1;
            text: "Meaningful: the caption carries what the picture shows.";
            color: Theme.foreground;
            font-size: Theme.font-size-sm;
            vertical-alignment: center;
        }
    }
    HorizontalLayout {
        spacing: 12px;
        alignment: center;
        ImageView {
            width: 48px;
            height: 48px;
            radius: Theme.radius-md;
            fit: ImageFit.contain;
            source: @image-url("../icons/sparkles.svg");
        }
        Text {
            horizontal-stretch: 1;
            text: "Decorative: the text already says it, so the picture adds nothing to repeat.";
            color: Theme.muted-foreground;
            font-size: Theme.font-size-sm;
            vertical-alignment: center;
        }
    }
}