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}