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