tessera_ui_basic_components/image.rs
1//! This module provides the `image` component and related utilities for rendering images in Tessera UI.
2//!
3//! It supports loading image data from file paths or raw bytes, decoding them into a format suitable for GPU rendering,
4//! and displaying them as part of the UI component tree. The main entry point is the [`image()`] component, which can be
5//! sized explicitly or use the intrinsic dimensions of the image. Image data should be loaded and decoded outside the
6//! main UI loop for optimal performance, using [`load_image_from_source`].
7//!
8//! Typical use cases include displaying static images, icons, or dynamically loaded pictures in UI layouts.
9//! The module is designed to integrate seamlessly with Tessera's stateless component model and rendering pipeline.
10
11use std::sync::Arc;
12
13use derive_builder::Builder;
14use image::GenericImageView;
15use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, tessera};
16
17use crate::pipelines::image::ImageCommand;
18
19pub use crate::pipelines::image::ImageData;
20
21/// Specifies the source for image data, which can be either a file path or raw bytes.
22///
23/// This enum is used by [`load_image_from_source`] to load image data from different sources.
24#[derive(Clone, Debug)]
25pub enum ImageSource {
26 /// Load image from a file path.
27 Path(String),
28 /// Load image from a byte slice. The data is wrapped in an `Arc` for efficient sharing.
29 Bytes(Arc<[u8]>),
30}
31
32/// Decodes an image from a given [`ImageSource`].
33///
34/// This function handles the loading and decoding of the image data into a format
35/// suitable for rendering. It should be called outside of the main UI loop or
36/// a component's `measure` closure to avoid performance degradation from decoding
37/// the image on every frame.
38///
39/// # Arguments
40///
41/// * `source` - A reference to the [`ImageSource`] to load the image from.
42///
43/// # Returns
44///
45/// A `Result` containing the decoded [`ImageData`] on success, or an `image::ImageError`
46/// on failure.
47pub fn load_image_from_source(source: &ImageSource) -> Result<ImageData, image::ImageError> {
48 let decoded = match source {
49 ImageSource::Path(path) => image::open(path)?,
50 ImageSource::Bytes(bytes) => image::load_from_memory(bytes)?,
51 };
52 let (width, height) = decoded.dimensions();
53 Ok(ImageData {
54 data: Arc::new(decoded.to_rgba8().into_raw()),
55 width,
56 height,
57 })
58}
59
60/// Arguments for the `image` component.
61///
62/// This struct holds the data and layout properties for an `image` component.
63/// It is typically created using the [`ImageArgsBuilder`] or by converting from [`ImageData`].
64#[derive(Debug, Builder, Clone)]
65#[builder(pattern = "owned")]
66pub struct ImageArgs {
67 /// The decoded image data, represented by [`ImageData`]. This contains the raw pixel
68 /// buffer and the image's dimensions.
69 #[builder(setter(into))]
70 pub data: Arc<ImageData>,
71
72 /// Explicit width for the image.
73 #[builder(default = "DimensionValue::WRAP", setter(into))]
74 pub width: DimensionValue,
75
76 /// Explicit height for the image.
77 #[builder(default = "DimensionValue::WRAP", setter(into))]
78 pub height: DimensionValue,
79}
80
81impl From<ImageData> for ImageArgs {
82 fn from(data: ImageData) -> Self {
83 ImageArgsBuilder::default()
84 .data(Arc::new(data))
85 .build()
86 .unwrap()
87 }
88}
89
90/// A component that renders an image.
91///
92/// The `image` component displays an image based on the provided [`ImageData`].
93/// It can be explicitly sized or automatically adjust to the intrinsic dimensions
94/// of the image. For optimal performance, image data should be loaded and decoded
95/// before being passed to this component, for example, by using the
96/// [`load_image_from_source`] function.
97///
98/// # Arguments
99///
100/// * `args` - The arguments for the image component, which can be an instance of
101/// [`ImageArgs`] or anything that converts into it (e.g., [`ImageData`]).
102///
103/// # Example
104///
105/// ```no_run
106/// use std::sync::Arc;
107/// use tessera_ui_basic_components::{
108/// image::{image, load_image_from_source, ImageArgsBuilder, ImageSource, ImageData},
109/// };
110/// use tessera_ui::{Dp, DimensionValue};
111///
112/// // In a real application, you would load the image data once and store it.
113/// // The `include_bytes!` macro is used here to load file contents at compile time.
114/// // For dynamic loading from a file path, you could use `ImageSource::Path`.
115/// let image_bytes = Arc::new(*include_bytes!("../../example/examples/assets/scarlet_ut.jpg"));
116/// let image_data = load_image_from_source(&ImageSource::Bytes(image_bytes))
117/// .expect("Failed to load image");
118///
119/// // Renders the image with its intrinsic size by passing `ImageData` directly.
120/// image(image_data.clone());
121///
122/// // Renders the image with a fixed width using `ImageArgs`.
123/// image(
124/// ImageArgsBuilder::default()
125/// .data(image_data)
126/// .width(DimensionValue::Fixed(Dp(100.0).into()))
127/// .build()
128/// .unwrap(),
129/// );
130/// ```
131#[tessera]
132pub fn image(args: impl Into<ImageArgs>) {
133 let image_args: ImageArgs = args.into();
134
135 measure(Box::new(move |input| {
136 let intrinsic_width = Px(image_args.data.width as i32);
137 let intrinsic_height = Px(image_args.data.height as i32);
138
139 let image_intrinsic_width = image_args.width;
140 let image_intrinsic_height = image_args.height;
141
142 let image_intrinsic_constraint =
143 Constraint::new(image_intrinsic_width, image_intrinsic_height);
144 let effective_image_constraint = image_intrinsic_constraint.merge(input.parent_constraint);
145
146 let width = match effective_image_constraint.width {
147 DimensionValue::Fixed(value) => value,
148 DimensionValue::Wrap { min, max } => min
149 .unwrap_or(Px(0))
150 .max(intrinsic_width)
151 .min(max.unwrap_or(Px::MAX)),
152 DimensionValue::Fill { min, max } => {
153 let parent_max = input.parent_constraint.width.get_max().unwrap_or(Px::MAX);
154 max.unwrap_or(parent_max)
155 .max(min.unwrap_or(Px(0)))
156 .max(intrinsic_width)
157 }
158 };
159
160 let height = match effective_image_constraint.height {
161 DimensionValue::Fixed(value) => value,
162 DimensionValue::Wrap { min, max } => min
163 .unwrap_or(Px(0))
164 .max(intrinsic_height)
165 .min(max.unwrap_or(Px::MAX)),
166 DimensionValue::Fill { min, max } => {
167 let parent_max = input.parent_constraint.height.get_max().unwrap_or(Px::MAX);
168 max.unwrap_or(parent_max)
169 .max(min.unwrap_or(Px(0)))
170 .max(intrinsic_height)
171 }
172 };
173
174 let image_command = ImageCommand {
175 data: image_args.data.clone(),
176 };
177
178 input
179 .metadatas
180 .entry(input.current_node_id)
181 .or_default()
182 .push_draw_command(image_command);
183
184 Ok(ComputedData { width, height })
185 }));
186}