Media Components
Media surfaces are first-class citizens in WaterUI. The waterui_media crate provides declarative
views for images (Photo), video playback (Video + VideoPlayer), Live Photos, and a unified
Media enum that dynamically chooses the right renderer. This chapter explores the API from basic
usage through advanced configuration.
Photos: Static Images with Placeholders
use waterui::prelude::*;
use waterui::components::media::Photo;
pub fn cover_image() -> impl View {
Photo::new("https://assets.waterui.dev/cover.png")
.placeholder(text("Loading…"))
}
Key features:
Photo::newaccepts anything convertible intowaterui_media::Url(web URLs,file://, etc.)..placeholder(view)renders while the backend fetches the asset..on_failure(view)handles network errors gracefully.- You can compose standard modifiers (
.padding(),.frame(...),.background(...)) around thePhotolike any other view.
Video Playback
Video represents a source, while VideoPlayer renders controls. Create one Video per asset and
reuse it if multiple players should point at the same file.
use waterui::prelude::*;
use waterui::components::media::{Video, VideoPlayer};
use waterui::reactive::binding;
pub fn trailer_player() -> impl View {
let video = Video::new("https://media.waterui.dev/trailer.mp4");
let muted = binding(false);
vstack((
VideoPlayer::new(video.clone()).muted(&muted),
button("Toggle Mute").action_with(&muted, |state| state.toggle()),
))
}
Muting Model
VideoPlayer::muted(&Binding<bool>)maps a boolean binding onto the player’s internal volume.VideoPlayerstores the pre-mute volume so toggling restores the last audible level.
Styling Considerations
The video chrome (play/pause controls) depends on the backend. SwiftUI renders native controls, whereas Web/Gtk4 use their respective toolkit widgets. Keep platform conventions in mind when layering overlays or gestures on top.
Live Photos
Apple’s Live Photos combine a still image and a short video clip. WaterUI packages the pair inside
LivePhotoSource:
use waterui::prelude::*;
use waterui::components::media::{LivePhoto, LivePhotoSource};
pub fn vacation_memory() -> impl View {
let source = LivePhotoSource::new(
"IMG_1024.jpg".into(),
"IMG_1024.mov".into(),
);
LivePhoto::new(source)
}
Backends that don’t support Live Photos fall back to the still image.
The Media Enum
When the media type is decided at runtime, wrap it in Media. Rendering becomes a single view
binding instead of a large match statement.
use waterui::prelude::*;
use waterui::components::media::Media;
use waterui::reactive::binding;
pub fn dynamic_media() -> impl View {
let media = binding(Media::Image("https://example.com/photo.png".into()));
// Later you can switch to Media::Video or Media::LivePhoto and the UI updates automatically.
media
}
Media implements View, so you can drop it directly into stacks, grids, or navigation views. To
switch the content, update the binding—WaterUI rebuilds the appropriate concrete view.
Media Picker (Feature Flag: media-picker)
Enable the crate feature in Cargo.toml:
[dependencies.waterui]
features = ["media-picker"]
Then present the picker:
use waterui::prelude::*;
use waterui::components::media::picker::{MediaFilter, MediaPicker, Selected};
use waterui::reactive::binding;
pub fn choose_photo() -> impl View {
let selection = binding::<Selected>(Selected(0));
MediaPicker::new()
.filter(MediaFilter::Image)
.selection(selection.clone())
}
The Selected binding stores an identifier. Use Selected::load() asynchronously (via task) to
receive the actual Media item and pipe it into your view tree.
use waterui::components::media::Media;
use waterui::reactive::binding;
use waterui::task::task;
let gallery = binding(Vec::<Media>::new());
button("Import").action_with(&selection, move |selected| {
let gallery = gallery.clone();
task(async move {
let media = selected.get().load().await;
gallery.push(media);
});
});
Best Practices
- Defer heavy processing – Image decoding and video playback happen in the backend. Avoid blocking the UI thread; let the renderer stream data.
- Provide fallbacks – Always set
.placeholderso the UI communicates status during network hiccups (future versions of the component will expose explicit failure hooks). - Reuse sources – Clone
Video/LivePhotoSourcehandles instead of recreating them in every recomposition. - Respect platform capabilities – Some backends may not implement Live Photos or media pickers yet. Feature-gate your UI or supply alternate paths.
With these components you can build media-heavy experiences—galleries, video players, immersive feeds—while keeping the code declarative and reactive.