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