tessera_ui_basic_components/
image.rs

1//! A component for rendering raster images.
2//!
3//! ## Usage
4//!
5//! Use to display static or dynamically loaded images.
6use std::sync::Arc;
7
8use derive_builder::Builder;
9use image::GenericImageView;
10use tessera_ui::{ComputedData, Constraint, DimensionValue, Px, tessera};
11
12use crate::pipelines::image::ImageCommand;
13
14pub use crate::pipelines::image::ImageData;
15
16/// Specifies the source for image data, which can be either a file path or raw bytes.
17///
18/// This enum is used by [`load_image_from_source`] to load image data from different sources.
19#[derive(Clone, Debug)]
20pub enum ImageSource {
21    /// Load image from a file path.
22    Path(String),
23    /// Load image from a byte slice. The data is wrapped in an `Arc` for efficient sharing.
24    Bytes(Arc<[u8]>),
25}
26
27/// Decodes an image from a given [`ImageSource`].
28///
29/// This function handles the loading and decoding of the image data into a format
30/// suitable for rendering. It should be called outside of the main UI loop or
31/// a component's `measure` closure to avoid performance degradation from decoding
32/// the image on every frame.
33///
34/// # Arguments
35///
36/// * `source` - A reference to the [`ImageSource`] to load the image from.
37///
38/// # Returns
39///
40/// A `Result` containing the decoded [`ImageData`] on success, or an `image::ImageError`
41/// on failure.
42pub fn load_image_from_source(source: &ImageSource) -> Result<ImageData, image::ImageError> {
43    let decoded = match source {
44        ImageSource::Path(path) => image::open(path)?,
45        ImageSource::Bytes(bytes) => image::load_from_memory(bytes)?,
46    };
47    let (width, height) = decoded.dimensions();
48    Ok(ImageData {
49        data: Arc::new(decoded.to_rgba8().into_raw()),
50        width,
51        height,
52    })
53}
54
55/// Arguments for the `image` component.
56///
57/// This struct holds the data and layout properties for an `image` component.
58/// It is typically created using the [`ImageArgsBuilder`] or by converting from [`ImageData`].
59#[derive(Debug, Builder, Clone)]
60#[builder(pattern = "owned")]
61pub struct ImageArgs {
62    /// The decoded image data, represented by [`ImageData`]. This contains the raw pixel
63    /// buffer and the image's dimensions.
64    #[builder(setter(into))]
65    pub data: Arc<ImageData>,
66
67    /// Explicit width for the image.
68    #[builder(default = "DimensionValue::WRAP", setter(into))]
69    pub width: DimensionValue,
70
71    /// Explicit height for the image.
72    #[builder(default = "DimensionValue::WRAP", setter(into))]
73    pub height: DimensionValue,
74}
75
76impl From<ImageData> for ImageArgs {
77    fn from(data: ImageData) -> Self {
78        ImageArgsBuilder::default()
79            .data(Arc::new(data))
80            .build()
81            .unwrap()
82    }
83}
84
85/// # image
86///
87/// Renders a raster image, fitting it to the available space or its intrinsic size.
88///
89/// ## Usage
90///
91/// Display a static asset or a dynamically loaded image from a file or memory.
92///
93/// ## Parameters
94///
95/// - `args` — configures the image data and layout; see [`ImageArgs`].
96///
97/// ## Examples
98///
99/// ```no_run
100/// use std::sync::Arc;
101/// use tessera_ui_basic_components::image::{
102///     image, load_image_from_source, ImageArgsBuilder, ImageSource,
103/// };
104///
105/// // In a real app, you might load image bytes from a file at runtime.
106/// // For this example, we include the bytes at compile time.
107/// let image_bytes = Arc::new(*include_bytes!("../../assets/counter.png"));
108/// let image_data = load_image_from_source(&ImageSource::Bytes(image_bytes))
109///     .expect("Failed to load image");
110///
111/// // Render the image using its loaded data.
112/// image(image_data);
113/// ```
114#[tessera]
115pub fn image(args: impl Into<ImageArgs>) {
116    let image_args: ImageArgs = args.into();
117
118    measure(Box::new(move |input| {
119        let intrinsic_width = Px(image_args.data.width as i32);
120        let intrinsic_height = Px(image_args.data.height as i32);
121
122        let image_intrinsic_width = image_args.width;
123        let image_intrinsic_height = image_args.height;
124
125        let image_intrinsic_constraint =
126            Constraint::new(image_intrinsic_width, image_intrinsic_height);
127        let effective_image_constraint = image_intrinsic_constraint.merge(input.parent_constraint);
128
129        let width = match effective_image_constraint.width {
130            DimensionValue::Fixed(value) => value,
131            DimensionValue::Wrap { min, max } => min
132                .unwrap_or(Px(0))
133                .max(intrinsic_width)
134                .min(max.unwrap_or(Px::MAX)),
135            DimensionValue::Fill { min, max } => {
136                let parent_max = input.parent_constraint.width.get_max().unwrap_or(Px::MAX);
137                max.unwrap_or(parent_max)
138                    .max(min.unwrap_or(Px(0)))
139                    .max(intrinsic_width)
140            }
141        };
142
143        let height = match effective_image_constraint.height {
144            DimensionValue::Fixed(value) => value,
145            DimensionValue::Wrap { min, max } => min
146                .unwrap_or(Px(0))
147                .max(intrinsic_height)
148                .min(max.unwrap_or(Px::MAX)),
149            DimensionValue::Fill { min, max } => {
150                let parent_max = input.parent_constraint.height.get_max().unwrap_or(Px::MAX);
151                max.unwrap_or(parent_max)
152                    .max(min.unwrap_or(Px(0)))
153                    .max(intrinsic_height)
154            }
155        };
156
157        let image_command = ImageCommand {
158            data: image_args.data.clone(),
159        };
160
161        input
162            .metadatas
163            .entry(input.current_node_id)
164            .or_default()
165            .push_draw_command(image_command);
166
167        Ok(ComputedData { width, height })
168    }));
169}