1use frus_core::{Color, Rect, Scene};
6use frus_layout::{Dimension, Style};
7
8use crate::icons::{AnimatedIconData, IconData, GRID};
9use crate::interaction::Status;
10use crate::theme::Theme;
11use crate::widget::Widget;
12
13pub struct Icon {
16 icon: IconData,
17 morph: Option<(AnimatedIconData, f32)>,
20 size: Option<f32>,
22 color: Option<Color>,
23}
24
25impl Icon {
26 pub fn new(icon: IconData) -> Self {
29 Self {
30 icon,
31 morph: None,
32 size: None,
33 color: None,
34 }
35 }
36
37 pub fn animated(icon: AnimatedIconData, on: bool) -> Self {
53 let target = if on { 1.0 } else { 0.0 };
54 Self {
55 icon: icon.at(target),
58 morph: Some((icon, target)),
59 size: None,
60 color: None,
61 }
62 }
63
64 pub fn size(mut self, size: f32) -> Self {
66 self.size = Some(size);
67 self
68 }
69
70 pub fn color(mut self, color: Color) -> Self {
72 self.color = Some(color);
73 self
74 }
75
76 fn sized(&self, side: f32) -> Style {
78 Style {
79 width: Dimension::Length(side),
80 height: Dimension::Length(side),
81 ..Default::default()
82 }
83 }
84
85 fn resolved_size(&self, theme: Option<&Theme>) -> f32 {
87 self.size
88 .or_else(|| theme.and_then(|t| t.widgets.icon.size))
89 .unwrap_or(GRID)
90 }
91}
92
93impl<Msg> Widget<Msg> for Icon {
94 fn style(&self) -> Style {
95 self.sized(self.resolved_size(None))
96 }
97
98 fn style_themed(&self, theme: &Theme) -> Style {
102 self.sized(self.resolved_size(Some(theme)))
103 }
104
105 fn children(&self) -> &[Box<dyn Widget<Msg>>] {
106 &[]
107 }
108
109 fn paint(&self, bounds: Rect, status: Status, theme: &Theme, scene: &mut Scene) {
110 let color = self
111 .color
112 .or(theme.widgets.icon.color)
113 .unwrap_or(theme.on_surface)
114 .fade(status.opacity);
115 let size = self.resolved_size(Some(theme));
116 let ox = bounds.x + (bounds.width - size) * 0.5;
119 let oy = bounds.y + (bounds.height - size) * 0.5;
120 let drawn = match self.morph {
122 Some((pair, _)) => pair.at(status.value),
123 None => self.icon,
124 };
125 let path = drawn.placed(size, ox, oy, theme.direction);
126 scene.fill_path(&path, color);
127 }
128
129 fn on_click(&self) -> Option<Msg> {
130 None
131 }
132
133 fn anim_target(&self) -> Option<f32> {
136 self.morph.map(|(_, target)| target)
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::icons::Icons;
144 use frus_core::Primitive;
145
146 fn paint_icon(icon: Icon) -> Vec<Primitive> {
147 let mut scene = Scene::new();
148 Widget::<()>::paint(
149 &icon,
150 Rect::new(0.0, 0.0, 24.0, 24.0),
151 Status::default(),
152 &Theme::default(),
153 &mut scene,
154 );
155 scene.primitives().to_vec()
156 }
157
158 #[test]
159 fn paints_a_single_filled_path() {
160 let prims = paint_icon(Icon::new(Icons::STAR));
161 assert_eq!(prims.len(), 1);
162 assert!(matches!(
163 prims[0],
164 Primitive::Path {
165 fill: Some(_),
166 stroke: None,
167 ..
168 }
169 ));
170 }
171
172 #[test]
173 fn color_override_beats_theme() {
174 let prims = paint_icon(Icon::new(Icons::FAVORITE).color(Color::rgb(1.0, 0.0, 0.0)));
175 match &prims[0] {
176 Primitive::Path { fill: Some(c), .. } => {
177 assert_eq!(c.r, 1.0);
178 assert_eq!(c.g, 0.0);
179 }
180 _ => panic!("expected a filled path"),
181 }
182 }
183
184 #[test]
188 fn a_morphing_icon_is_painted_where_the_animation_has_got_to() {
189 let paint_at = |value: f32| {
190 let mut scene = Scene::new();
191 let status = Status {
192 value,
193 ..Status::default()
194 };
195 Widget::<()>::paint(
196 &Icon::animated(crate::AnimatedIcons::MENU_CLOSE, true),
197 Rect::new(0.0, 0.0, 24.0, 24.0),
198 status,
199 &Theme::default(),
200 &mut scene,
201 );
202 match scene.primitives().first() {
203 Some(Primitive::Path { path, .. }) => path.clone(),
204 other => panic!("expected a filled path, got {other:?}"),
205 }
206 };
207 let (shut, half, open) = (paint_at(0.0), paint_at(0.5), paint_at(1.0));
208 assert_ne!(shut.verbs(), half.verbs(), "half way is not the start");
209 assert_ne!(half.verbs(), open.verbs(), "and it is not the end either");
210 assert_eq!(
211 shut.verbs().len(),
212 half.verbs().len(),
213 "the same shape all the way across"
214 );
215 }
216
217 #[test]
221 fn only_a_morphing_icon_asks_the_runtime_for_a_value() {
222 let open = Icon::animated(crate::AnimatedIcons::MENU_CLOSE, true);
223 let shut = Icon::animated(crate::AnimatedIcons::MENU_CLOSE, false);
224 assert_eq!(Widget::<()>::anim_target(&open), Some(1.0));
225 assert_eq!(Widget::<()>::anim_target(&shut), Some(0.0));
226 assert_eq!(Widget::<()>::anim_target(&Icon::new(Icons::STAR)), None);
227 }
228
229 #[test]
230 fn size_drives_the_layout_box() {
231 let icon = Icon::new(Icons::CHECK).size(40.0);
232 let style = Widget::<()>::style(&icon);
233 assert_eq!(style.width, Dimension::Length(40.0));
234 assert_eq!(style.height, Dimension::Length(40.0));
235 }
236}