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}