Skip to main content

fission_core/ui/widgets/
icon.rs

1use crate::lowering::{LoweringContext, NodeBuilder};
2use crate::ui::traits::Lower;
3use fission_ir::{
4    op::{Color, Fill, LayoutOp, Op, PaintOp, Stroke},
5    NodeId,
6};
7use serde::{Deserialize, Serialize};
8
9/// The source of an [`Icon`]'s vector graphic.
10///
11/// - `Path` -- an SVG path data string (e.g. `"M12 2L2 22h20L12 2z"`).
12/// - `File` -- a filesystem path to an SVG file.
13/// - `SvgContent` -- inline SVG markup.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum IconSource {
16    /// SVG path data string (`d` attribute content).
17    Path(String),
18    /// Filesystem path to an SVG file.
19    File(String),
20    /// Complete inline SVG markup.
21    SvgContent(String),
22}
23
24impl Default for IconSource {
25    fn default() -> Self {
26        IconSource::Path(String::new())
27    }
28}
29
30impl From<String> for IconSource {
31    fn from(s: String) -> Self {
32        IconSource::Path(s)
33    }
34}
35
36/// A vector icon rendered from an SVG path, file, or inline SVG content.
37///
38/// Icons default to the theme's primary text colour and 24x24 layout points.
39/// Use `color()`, `size()`, and `stroke()` to customise.
40///
41/// # Example
42///
43/// ```rust,ignore
44/// // From an SVG path string
45/// Icon::path("M12 2L2 22h20L12 2z")
46///     .size(20.0)
47///     .color(theme.tokens.colors.primary)
48///
49/// // From a file
50/// Icon::file("assets/icons/star.svg").size(16.0)
51///
52/// // From inline SVG
53/// Icon::svg("<svg>...</svg>")
54/// ```
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Icon {
57    /// Explicit node identity.
58    pub id: Option<NodeId>,
59    /// The vector graphic source.
60    pub source: IconSource,
61    /// Fill colour (falls back to the theme's primary text colour).
62    pub color: Option<Color>,
63    /// Layout size in points (default: 24.0).
64    pub size: Option<f32>,
65    /// Optional stroke (when set, the fill is suppressed).
66    pub stroke: Option<Stroke>,
67}
68
69impl Icon {
70    pub fn path(path: impl Into<String>) -> Self {
71        Self {
72            id: None,
73            source: IconSource::Path(path.into()),
74            color: None,
75            size: None,
76            stroke: None,
77        }
78    }
79
80    pub fn file(path: impl Into<String>) -> Self {
81        Self {
82            id: None,
83            source: IconSource::File(path.into()),
84            color: None,
85            size: None,
86            stroke: None,
87        }
88    }
89
90    pub fn svg(content: impl Into<String>) -> Self {
91        Self {
92            id: None,
93            source: IconSource::SvgContent(content.into()),
94            color: None,
95            size: None,
96            stroke: None,
97        }
98    }
99    
100    // Deprecated: new -> path
101    pub fn new(path: impl Into<String>) -> Self {
102        Self::path(path)
103    }
104    
105    pub fn size(mut self, s: f32) -> Self {
106        self.size = Some(s);
107        self
108    }
109    
110    pub fn color(mut self, c: Color) -> Self {
111        self.color = Some(c);
112        self
113    }
114    
115    pub fn stroke(mut self, s: Stroke) -> Self {
116        self.stroke = Some(s);
117        self
118    }
119
120    pub fn into_node(self) -> crate::ui::Node {
121        crate::ui::Node::Icon(self)
122    }
123}
124
125impl Lower for Icon {
126    fn lower(&self, cx: &mut LoweringContext) -> NodeId {
127        let id = self.id.unwrap_or_else(|| cx.next_node_id());
128        
129        let tokens = &cx.env.theme.tokens;
130        let color = self.color.unwrap_or(tokens.colors.text_primary);
131        let size = self.size.unwrap_or(24.0);
132
133        // Paint Op
134        let paint_op = match &self.source {
135            IconSource::Path(d) => PaintOp::DrawPath {
136                path: d.clone(),
137                fill: if self.stroke.is_some() { None } else { Some(Fill { color }) },
138                stroke: self.stroke,
139            },
140            IconSource::File(f) => {
141                let content = std::fs::read_to_string(f).unwrap_or_default();
142                PaintOp::DrawSvg {
143                    content,
144                    fill: if self.stroke.is_some() { None } else { Some(Fill { color }) },
145                    stroke: self.stroke,
146                }
147            },
148            IconSource::SvgContent(c) => PaintOp::DrawSvg {
149                content: c.clone(),
150                fill: if self.stroke.is_some() { None } else { Some(Fill { color }) },
151                stroke: self.stroke,
152            },
153        };
154
155        let paint_id = NodeBuilder::new(cx.next_node_id(), Op::Paint(paint_op)).build(cx);
156
157        let mut layout = NodeBuilder::new(
158            id,
159            Op::Layout(LayoutOp::Box {
160                width: Some(size),
161                height: Some(size),
162                min_width: None, max_width: None, min_height: None, max_height: None,
163                padding: [0.0; 4],
164                flex_grow: 0.0,
165                flex_shrink: 0.0,
166                aspect_ratio: None,
167            }),
168        );
169        layout.add_child(paint_id);
170        layout.build(cx)
171    }
172}