tessera_ui_basic_components/
icon.rs

1//! A component for rendering raster or vector icons.
2//!
3//! ## Usage
4//!
5//! Use to display a scalable icon from image or vector data.
6use std::sync::Arc;
7
8use derive_builder::Builder;
9use tessera_ui::{Color, ComputedData, Constraint, DimensionValue, Dp, Px, tessera};
10
11use crate::pipelines::{
12    image::{ImageCommand, ImageData},
13    image_vector::{ImageVectorCommand, ImageVectorData},
14};
15
16/// Icon content can be provided either as vector geometry or raster pixels.
17#[derive(Debug, Clone)]
18pub enum IconContent {
19    /// Render the icon via the vector pipeline.
20    Vector(Arc<ImageVectorData>),
21    /// Render the icon via the raster image pipeline.
22    Raster(Arc<ImageData>),
23}
24
25impl From<ImageVectorData> for IconContent {
26    fn from(data: ImageVectorData) -> Self {
27        Self::Vector(Arc::new(data))
28    }
29}
30
31impl From<Arc<ImageVectorData>> for IconContent {
32    fn from(data: Arc<ImageVectorData>) -> Self {
33        Self::Vector(data)
34    }
35}
36
37impl From<ImageData> for IconContent {
38    fn from(data: ImageData) -> Self {
39        Self::Raster(Arc::new(data))
40    }
41}
42
43impl From<Arc<ImageData>> for IconContent {
44    fn from(data: Arc<ImageData>) -> Self {
45        Self::Raster(data)
46    }
47}
48
49/// Arguments for the [`icon`] component.
50#[derive(Debug, Builder, Clone)]
51#[builder(pattern = "owned")]
52pub struct IconArgs {
53    /// Icon content, provided as either raster pixels or vector geometry.
54    #[builder(setter(into))]
55    pub content: IconContent,
56    /// Logical size of the icon. Applied to both width and height unless explicit overrides
57    /// are provided through [`width`](IconArgs::width) / [`height`](IconArgs::height).
58    #[builder(default = "Dp(24.0)")]
59    pub size: Dp,
60    /// Optional width override. Handy when the icon should `Fill` or `Wrap` differently from
61    /// the default square sizing.
62    #[builder(default, setter(strip_option))]
63    pub width: Option<DimensionValue>,
64    /// Optional height override. Handy when the icon should `Fill` or `Wrap` differently from
65    /// the default square sizing.
66    #[builder(default, setter(strip_option))]
67    pub height: Option<DimensionValue>,
68    /// Tint color applied to vector icons. Defaults to white so it preserves the original
69    /// colors (multiplying by white is a no-op). Raster icons ignore this field.
70    #[builder(default = "Color::WHITE")]
71    pub tint: Color,
72}
73
74impl From<IconContent> for IconArgs {
75    fn from(content: IconContent) -> Self {
76        IconArgsBuilder::default()
77            .content(content)
78            .build()
79            .expect("IconArgsBuilder failed with required fields set")
80    }
81}
82
83impl From<ImageVectorData> for IconArgs {
84    fn from(data: ImageVectorData) -> Self {
85        IconContent::from(data).into()
86    }
87}
88
89impl From<Arc<ImageVectorData>> for IconArgs {
90    fn from(data: Arc<ImageVectorData>) -> Self {
91        IconContent::from(data).into()
92    }
93}
94
95impl From<ImageData> for IconArgs {
96    fn from(data: ImageData) -> Self {
97        IconContent::from(data).into()
98    }
99}
100
101impl From<Arc<ImageData>> for IconArgs {
102    fn from(data: Arc<ImageData>) -> Self {
103        IconContent::from(data).into()
104    }
105}
106
107/// # icon
108///
109/// Renders an icon with consistent sizing and optional tinting for vectors.
110///
111/// ## Usage
112///
113/// Display a vector or raster image with a uniform size, often inside a button or as a status indicator.
114///
115/// ## Parameters
116///
117/// - `args` — configures the icon's content, size, and tint; see [`IconArgs`].
118///
119/// ## Examples
120///
121/// ```no_run
122/// use std::sync::Arc;
123/// use tessera_ui::Color;
124/// use tessera_ui_basic_components::{
125///     icon::{icon, IconArgsBuilder},
126///     image_vector::{ImageVectorSource, load_image_vector_from_source},
127/// };
128///
129/// // Load vector data from an SVG file.
130/// // In a real app, this should be done once and the data cached.
131/// let svg_path = "../assets/emoji_u1f416.svg";
132/// let vector_data = load_image_vector_from_source(
133///     &ImageVectorSource::Path(svg_path.to_string())
134/// ).unwrap();
135///
136/// icon(
137///     IconArgsBuilder::default()
138///         .content(vector_data)
139///         .tint(Color::new(0.2, 0.5, 0.8, 1.0))
140///         .build()
141///         .unwrap(),
142/// );
143/// ```
144#[tessera]
145pub fn icon(args: impl Into<IconArgs>) {
146    let icon_args: IconArgs = args.into();
147
148    measure(Box::new(move |input| {
149        let (intrinsic_width, intrinsic_height) = intrinsic_dimensions(&icon_args.content);
150        let size_px = icon_args.size.to_px();
151
152        let preferred_width = icon_args.width.unwrap_or(DimensionValue::Fixed(size_px));
153        let preferred_height = icon_args.height.unwrap_or(DimensionValue::Fixed(size_px));
154
155        let constraint = Constraint::new(preferred_width, preferred_height);
156        let effective_constraint = constraint.merge(input.parent_constraint);
157
158        let width = match effective_constraint.width {
159            DimensionValue::Fixed(value) => value,
160            DimensionValue::Wrap { min, max } => min
161                .unwrap_or(Px(0))
162                .max(intrinsic_width)
163                .min(max.unwrap_or(Px::MAX)),
164            DimensionValue::Fill { min, max } => {
165                let parent_max = input.parent_constraint.width.get_max().unwrap_or(Px::MAX);
166                max.unwrap_or(parent_max)
167                    .max(min.unwrap_or(Px(0)))
168                    .max(intrinsic_width)
169            }
170        };
171
172        let height = match effective_constraint.height {
173            DimensionValue::Fixed(value) => value,
174            DimensionValue::Wrap { min, max } => min
175                .unwrap_or(Px(0))
176                .max(intrinsic_height)
177                .min(max.unwrap_or(Px::MAX)),
178            DimensionValue::Fill { min, max } => {
179                let parent_max = input.parent_constraint.height.get_max().unwrap_or(Px::MAX);
180                max.unwrap_or(parent_max)
181                    .max(min.unwrap_or(Px(0)))
182                    .max(intrinsic_height)
183            }
184        };
185
186        match &icon_args.content {
187            IconContent::Vector(data) => {
188                let command = ImageVectorCommand {
189                    data: data.clone(),
190                    tint: icon_args.tint,
191                };
192                input
193                    .metadatas
194                    .entry(input.current_node_id)
195                    .or_default()
196                    .push_draw_command(command);
197            }
198            IconContent::Raster(data) => {
199                let command = ImageCommand { data: data.clone() };
200                input
201                    .metadatas
202                    .entry(input.current_node_id)
203                    .or_default()
204                    .push_draw_command(command);
205            }
206        }
207
208        Ok(ComputedData { width, height })
209    }));
210}
211
212fn intrinsic_dimensions(content: &IconContent) -> (Px, Px) {
213    match content {
214        IconContent::Vector(data) => (
215            px_from_f32(data.viewport_width),
216            px_from_f32(data.viewport_height),
217        ),
218        IconContent::Raster(data) => (clamp_u32_to_px(data.width), clamp_u32_to_px(data.height)),
219    }
220}
221
222fn px_from_f32(value: f32) -> Px {
223    let clamped = value.max(0.0).min(i32::MAX as f32);
224    Px(clamped.round() as i32)
225}
226
227fn clamp_u32_to_px(value: u32) -> Px {
228    Px::new(value.min(i32::MAX as u32) as i32)
229}