1use std::ops::Deref;
6
7use oxframe_core::{Color, Signal};
8
9use crate::builder::{BaseProps, NodeBuilder, SvgBuilder};
10use crate::graph::{NodeId, SceneGraph};
11use crate::handles::{Node, Path, Svg};
12use crate::theme::{Ink, IntoPaint};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum IconStyle {
18 Stroke,
20 Fill,
22}
23
24#[derive(Debug)]
26#[must_use = "a builder does nothing until it is passed to `add`"]
27pub struct IconBuilder {
28 svg: SvgBuilder,
29 style: IconStyle,
30 size: f64,
31 color: Option<Color>,
32 stroke_width: f64,
33}
34
35impl IconBuilder {
36 pub fn size(mut self, v: f64) -> Self {
38 self.size = v;
39 self
40 }
41
42 pub fn color(mut self, v: impl IntoPaint) -> Self {
44 self.color = Some(v.into_paint());
45 self
46 }
47
48 pub fn stroke_width(mut self, v: f64) -> Self {
51 self.stroke_width = v;
52 self
53 }
54}
55
56impl NodeBuilder for IconBuilder {
57 type Handle = Icon;
58
59 fn base(&mut self) -> &mut BaseProps {
60 self.svg.base()
61 }
62
63 fn build(self, g: &SceneGraph, parent: NodeId) -> Icon {
64 let color = Signal::new(self.color.unwrap_or_else(|| Ink::Text.into_paint()));
65 let stroke_width = Signal::new(self.stroke_width);
66 let style = self.style;
67 let svg = self.svg.width(self.size).height(self.size).build(g, parent);
68 for path in g.descendant_paths(svg.id()) {
69 let n = g.node(path);
70 if n.stroke.peek().to_rgba8()[3] > 0 {
71 let c = color.clone();
72 n.stroke.bind(move || c.get());
73 let base = n.line_width.peek() / 2.0;
75 let w = stroke_width.clone();
76 n.line_width.bind(move || base * w.get());
77 }
78 if n.fill.peek().to_rgba8()[3] > 0 {
79 let c = color.clone();
80 n.fill.bind(move || c.get());
81 }
82 }
83 Icon {
84 svg,
85 style,
86 color,
87 stroke_width,
88 }
89 }
90}
91
92#[derive(Debug, Clone)]
95pub struct Icon {
96 svg: Svg,
97 style: IconStyle,
98 color: Signal<Color>,
99 stroke_width: Signal<f64>,
100}
101
102impl Icon {
103 pub fn from_svg(svg: &[u8], style: IconStyle) -> IconBuilder {
109 IconBuilder {
110 svg: Svg::new().src(svg),
111 style,
112 size: 48.0,
113 color: None,
114 stroke_width: 2.0,
115 }
116 }
117
118 pub fn node(&self) -> Node {
120 *self.svg
121 }
122
123 pub fn color(&self) -> Signal<Color> {
125 self.color.clone()
126 }
127
128 pub fn stroke_width(&self) -> Signal<f64> {
130 self.stroke_width.clone()
131 }
132
133 pub fn paths(&self) -> Vec<Path> {
135 self.svg.paths()
136 }
137
138 pub fn style(&self) -> IconStyle {
140 self.style
141 }
142}
143
144impl Deref for Icon {
145 type Target = Node;
146 fn deref(&self) -> &Node {
147 &self.svg
148 }
149}
150
151impl From<Icon> for Node {
152 fn from(h: Icon) -> Node {
153 *h.svg
154 }
155}
156
157impl From<&Icon> for Node {
158 fn from(h: &Icon) -> Node {
159 *h.svg
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::View;
167 use crate::graph::SceneGraph;
168
169 const ARROW: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>"##;
170 const DOT: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><circle cx="12" cy="12" r="8"/></svg>"##;
171
172 #[test]
173 fn stroke_icon_follows_its_signals() {
174 SceneGraph::new(200, 200).enter(|| {
175 let icon = View::current().add(
176 Icon::from_svg(ARROW.as_bytes(), IconStyle::Stroke)
177 .size(96.0)
178 .color(Color::rgb(1.0, 0.0, 0.0)),
179 );
180 assert_eq!(icon.size(), (96.0, 96.0));
181 let paths = Svg::from_id(icon.id()).paths();
182 assert_eq!(paths.len(), 2);
183 assert_eq!(paths[0].stroke().get(), Color::rgb(1.0, 0.0, 0.0));
184 assert!((paths[0].line_width().get() - 8.0).abs() < 1e-6);
185 icon.color().set(Color::WHITE);
186 icon.stroke_width().set(1.0);
187 assert_eq!(paths[1].stroke().get(), Color::WHITE);
188 assert!((paths[1].line_width().get() - 4.0).abs() < 1e-6);
189 assert_eq!(paths[1].fill().get().to_rgba8()[3], 0);
190 assert_eq!(icon.style(), IconStyle::Stroke);
191 });
192 }
193
194 #[test]
195 fn fill_icon_colors_its_fill() {
196 SceneGraph::new(200, 200).enter(|| {
197 let icon = View::current()
198 .add(Icon::from_svg(DOT.as_bytes(), IconStyle::Fill).color(Color::WHITE));
199 let paths = Svg::from_id(icon.id()).paths();
200 assert_eq!(paths[0].fill().get(), Color::WHITE);
201 assert_eq!(icon.style(), IconStyle::Fill);
202 });
203 }
204}