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.
#Start with a working image
// 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
// `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)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())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.
#Gallery with selection
// 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
// 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.
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;
}
}
}