Skip to main content

tessera_components/
icon.rs

1//! Icon component for rendering vector or raster imagery.
2//!
3//! ## Usage
4//!
5//! Use to display a tintable symbol in buttons, tabs, and status indicators.
6use std::sync::Arc;
7
8use tessera_ui::{
9    AxisConstraint, Color, ComputedData, Dp, LayoutResult, MeasurementError, Px,
10    layout::{MeasureScope, layout},
11    tessera, use_context,
12};
13
14use crate::{
15    image::clamp_f32_to_px,
16    image_vector::TintMode,
17    painter::Painter,
18    pipelines::{
19        image::command::{ImageCommand, ImageData},
20        image_vector::command::ImageVectorCommand,
21    },
22    theme::{ContentColor, MaterialTheme},
23};
24
25#[derive(Clone, Copy, Debug, PartialEq)]
26pub(crate) struct IconSize(pub Dp);
27
28#[derive(Clone, Copy, Debug, PartialEq)]
29pub(crate) struct IconRotation(pub f32);
30
31struct PlaceholderRaster(Arc<ImageData>);
32
33impl Default for PlaceholderRaster {
34    fn default() -> Self {
35        Self(Arc::new(ImageData::from_rgba(vec![0, 0, 0, 0], 1, 1)))
36    }
37}
38
39fn placeholder_raster_data() -> Arc<ImageData> {
40    Arc::clone(&tessera_ui::__private::runtime_resource::<PlaceholderRaster>().0)
41}
42
43impl IconBuilder {
44    /// Sets icon content using a shared painter value.
45    pub fn painter(&self, painter: impl Into<Painter>) -> Self {
46        let value = Some(painter.into());
47        self.props
48            .configure("content", move |props| props.content = value);
49        Self { props: self.props }
50    }
51}
52
53#[derive(Clone, PartialEq)]
54struct IconLayout {
55    content: Painter,
56    size: Dp,
57    width: Option<AxisConstraint>,
58    height: Option<AxisConstraint>,
59    tint: Color,
60    tint_mode: TintMode,
61    rotation: f32,
62}
63
64impl tessera_ui::LayoutModifierNode for IconLayout {
65    fn measure_eq(&self, other: &Self) -> bool {
66        self.size == other.size
67            && self.width == other.width
68            && self.height == other.height
69            && (self.width.is_none() && self.height.is_none()
70                || intrinsic_dimensions(&self.content) == intrinsic_dimensions(&other.content))
71    }
72
73    fn placement_eq(&self, other: &Self) -> bool {
74        self.measure_eq(other)
75    }
76
77    fn measure(&self, input: &mut MeasureScope<'_>) -> Result<LayoutResult, MeasurementError> {
78        let (intrinsic_width, intrinsic_height) = intrinsic_dimensions(&self.content);
79        let size_px = self.size.to_px();
80
81        let preferred_width = self.width.unwrap_or(AxisConstraint::exact(size_px));
82        let preferred_height = self.height.unwrap_or(AxisConstraint::exact(size_px));
83        let width = input
84            .parent_constraint()
85            .width()
86            .clamp(preferred_width.clamp(intrinsic_width));
87        let height = input
88            .parent_constraint()
89            .height()
90            .clamp(preferred_height.clamp(intrinsic_height));
91
92        Ok(LayoutResult::new(ComputedData { width, height }))
93    }
94}
95
96impl tessera_ui::DrawModifierNode for IconLayout {
97    fn draw(
98        &self,
99        ctx: &mut tessera_ui::DrawModifierContext<'_, '_>,
100        content: &mut dyn tessera_ui::DrawModifierContent,
101    ) {
102        let input = &mut *ctx.render_input;
103        let mut metadata = input.metadata_mut();
104        match &self.content {
105            Painter::Source(_) => {}
106            Painter::Vector(data) => {
107                let command = ImageVectorCommand {
108                    data: data.clone(),
109                    tint: self.tint,
110                    tint_mode: self.tint_mode,
111                    rotation: self.rotation,
112                };
113                metadata.fragment_mut().push_draw_command(command);
114            }
115            Painter::Raster(data) => {
116                let command = ImageCommand {
117                    data: data.clone(),
118                    opacity: 1.0,
119                };
120                metadata.fragment_mut().push_draw_command(command);
121            }
122        }
123        content.draw(input);
124    }
125}
126
127/// # icon
128///
129/// Renders an icon with consistent sizing and optional tinting for vectors.
130///
131/// ## Usage
132///
133/// Display a vector or raster symbol with uniform size, typically in controls
134/// and compact labels.
135///
136/// ## Parameters
137///
138/// - `content` - optional painter payload for vector or raster imagery.
139/// - `size` - optional preferred square size.
140/// - `width` / `height` - optional explicit layout dimensions.
141/// - `tint` - optional tint override for vector icons.
142/// - `tint_mode` - vector tint blending mode, defaulting to solid recoloring.
143/// - `rotation` - clockwise degrees, inheriting control motion when omitted.
144///
145/// ## Examples
146///
147/// ```
148/// # use tessera_ui::tessera;
149/// # #[tessera]
150/// # fn component() {
151/// use tessera_components::{icon::icon, material_icons::filled};
152/// use tessera_ui::Color;
153///
154/// icon()
155///     .painter(filled::STAR_SVG)
156///     .size(tessera_ui::Dp(20.0))
157///     .tint(Color::new(0.2, 0.5, 0.8, 1.0));
158/// # }
159/// ```
160#[tessera]
161pub fn icon(
162    #[prop(skip_setter)] content: Option<Painter>,
163    size: Option<Dp>,
164    width: Option<AxisConstraint>,
165    height: Option<AxisConstraint>,
166    tint: Option<Color>,
167    tint_mode: Option<TintMode>,
168    rotation: Option<f32>,
169) {
170    let content = content
171        .and_then(Painter::resolve)
172        .unwrap_or_else(|| Painter::Raster((*placeholder_raster_data()).clone()));
173    let default_size = use_context::<IconSize>()
174        .map(|size| size.get().0)
175        .unwrap_or(Dp(24.0));
176    let size = size.unwrap_or(default_size);
177    let tint = if let Some(tint) = tint {
178        tint
179    } else {
180        let theme = use_context::<MaterialTheme>();
181        use_context::<ContentColor>()
182            .map(|c| c.get().current)
183            .or_else(|| theme.map(|t| t.get().color_scheme.on_surface))
184            .unwrap_or_else(|| ContentColor::default().current)
185    };
186    let tint_mode = tint_mode.unwrap_or(TintMode::Solid);
187    let inherited_rotation = use_context::<IconRotation>()
188        .map(|value| value.get().0)
189        .unwrap_or(0.0);
190    let rotation = rotation.unwrap_or(inherited_rotation);
191    let policy = IconLayout {
192        content,
193        size,
194        width,
195        height,
196        tint,
197        tint_mode,
198        rotation,
199    };
200    layout().policy(policy.clone()).draw(policy);
201}
202
203fn intrinsic_dimensions(content: &Painter) -> (Px, Px) {
204    match content {
205        Painter::Source(_) => (Px::ZERO, Px::ZERO),
206        Painter::Vector(data) => (
207            clamp_f32_to_px(data.viewport_width),
208            clamp_f32_to_px(data.viewport_height),
209        ),
210        Painter::Raster(data) => (clamp_u32_to_px(data.width), clamp_u32_to_px(data.height)),
211    }
212}
213
214fn clamp_u32_to_px(value: u32) -> Px {
215    Px::new(value.min(i32::MAX as u32) as i32)
216}