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, 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() {
138                    None
139                } else {
140                    Some(fission_ir::op::Fill::Solid(color))
141                },
142                stroke: self.stroke.clone(),
143            },
144            IconSource::File(f) => {
145                let content = std::fs::read_to_string(f).unwrap_or_default();
146                PaintOp::DrawSvg {
147                    content,
148                    fill: if self.stroke.is_some() {
149                        None
150                    } else {
151                        Some(fission_ir::op::Fill::Solid(color))
152                    },
153                    stroke: self.stroke.clone(),
154                }
155            }
156            IconSource::SvgContent(c) => PaintOp::DrawSvg {
157                content: c.clone(),
158                fill: if self.stroke.is_some() {
159                    None
160                } else {
161                    Some(fission_ir::op::Fill::Solid(color))
162                },
163                stroke: self.stroke.clone(),
164            },
165        };
166
167        let paint_id = NodeBuilder::new(cx.next_node_id(), Op::Paint(paint_op)).build(cx);
168
169        let mut layout = NodeBuilder::new(
170            id,
171            Op::Layout(LayoutOp::Box {
172                width: Some(size),
173                height: Some(size),
174                min_width: None,
175                max_width: None,
176                min_height: None,
177                max_height: None,
178                padding: [0.0; 4],
179                flex_grow: 0.0,
180                flex_shrink: 0.0,
181                aspect_ratio: None,
182            }),
183        );
184        layout.add_child(paint_id);
185        layout.build(cx)
186    }
187}