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}