1use crate::animation::Animatable;
2use crate::media::effects::{IEffect, ImmutableBlurEffect, ImmutableDropShadowEffect};
3use crate::media::{Color, Colors};
4use crate::reactive::{Disposable, IDisposable};
5use crate::utilities::{FormatError, HandlerList};
6use crate::{ferro_class, ferro_impl_classes, FerroObject, FerroObjectImpl, FerroProperty, ObjectType, Upcast};
7use std::rc::Rc;
8
9#[repr(C)]
11pub struct Effect {
12 base: Animatable,
13 invalidated: HandlerList<dyn Fn()>,
14}
15
16ferro_class!(Effect: Animatable);
17crate::ferro_class_info!(Effect { interfaces: [std::rc::Rc<dyn crate::media::effects::IEffect>] });
18ferro_impl_classes!(Effect: FerroObjectImpl);
19
20impl Effect {
21 pub fn construct() -> Self {
23 Self { base: Animatable::construct(), invalidated: HandlerList::new() }
24 }
25
26 pub fn affects_render<T: ObjectType + Upcast<Effect>>(properties: &[&'static FerroProperty]) {
30 for property in properties {
31 property.changed().subscribe(|e| {
32 if let Some(sender) = e.sender().downcast_ref::<T>() {
33 let effect: &Effect = sender.upcast();
34 effect.raise_invalidated();
35 }
36 });
37 }
38 }
39
40 pub fn raise_invalidated(&self) {
42 if self.invalidated.is_empty() {
43 return;
44 }
45 for (_, handler) in self.invalidated.snapshot().iter() {
46 handler();
47 }
48 }
49
50 pub fn invalidated(&self, handler: impl Fn() + 'static) -> Rc<dyn IDisposable> {
54 let token = self.invalidated.add(Rc::new(handler));
55 let object: &FerroObject = self.upcast();
56 let weak = object.to_weak();
57 Disposable::create(move || {
58 if let Some(this) = weak.upgrade() {
59 if let Some(this) = this.downcast_ref::<Effect>() {
60 this.invalidated.remove(token);
61 }
62 }
63 })
64 }
65
66 fn parse_error(s: &str) -> FormatError {
67 FormatError::from_string(format!("Unable to parse effect: {s}"))
68 }
69
70 pub fn parse(s: &str) -> Result<Rc<dyn IEffect>, FormatError> {
73 let mut r = TokenParser::new(s);
74 if r.try_consume_str("blur") {
75 let radius = match (r.try_consume('('), r.try_parse_double()) {
76 (true, Some(radius)) if r.try_consume(')') && r.is_eof_with_whitespace() => radius,
77 _ => return Err(Self::parse_error(s)),
78 };
79 return Ok(Rc::new(ImmutableBlurEffect::new(radius)));
80 }
81
82 if r.try_consume_str("drop-shadow") {
83 if !r.try_consume('(') {
84 return Err(Self::parse_error(s));
85 }
86 let Some(offset_x) = r.try_parse_double() else { return Err(Self::parse_error(s)) };
87 let Some(offset_y) = r.try_parse_double() else { return Err(Self::parse_error(s)) };
88 let mut blur_radius = 0.0;
89 let color = Colors::BLACK;
90 if !r.try_consume(')') {
91 match r.try_parse_double() {
92 Some(value) if value >= 0.0 => blur_radius = value,
93 _ => return Err(Self::parse_error(s)),
94 }
95
96 if !r.try_consume(')') {
97 let Some(end_of_expression) = s.rfind(')') else { return Err(Self::parse_error(s)) };
98
99 if !TokenParser::new(&s[end_of_expression + 1..]).is_eof_with_whitespace() {
100 return Err(Self::parse_error(s));
101 }
102
103 if end_of_expression < r.position() {
104 return Err(Self::parse_error(s));
105 }
106 let Some(color) = Color::try_parse(s[r.position()..end_of_expression].trim_end()) else {
107 return Err(Self::parse_error(s));
108 };
109
110 return Ok(Rc::new(ImmutableDropShadowEffect::new(offset_x, offset_y, blur_radius, color, 1.0)));
111 }
112 }
113 if !r.is_eof_with_whitespace() {
114 return Err(Self::parse_error(s));
115 }
116 return Ok(Rc::new(ImmutableDropShadowEffect::new(offset_x, offset_y, blur_radius, color, 1.0)));
117 }
118
119 Err(Self::parse_error(s))
120 }
121}
122
123struct TokenParser<'a> {
126 s: &'a str,
127 position: usize,
128}
129
130impl<'a> TokenParser<'a> {
131 fn new(s: &'a str) -> Self {
132 Self { s, position: 0 }
133 }
134
135 fn position(&self) -> usize {
137 self.position
138 }
139
140 fn skip_whitespace(&mut self) {
141 let trimmed = self.s.trim_start_matches(char::is_whitespace);
142 self.advance(self.s.len() - trimmed.len());
143 }
144
145 fn try_consume(&mut self, c: char) -> bool {
146 self.skip_whitespace();
147 if !self.s.starts_with(c) {
148 return false;
149 }
150 self.advance(c.len_utf8());
151 true
152 }
153
154 fn try_consume_str(&mut self, s: &str) -> bool {
155 self.skip_whitespace();
156 if !self.s.starts_with(s) {
157 return false;
158 }
159 self.advance(s.len());
160 true
161 }
162
163 fn advance(&mut self, c: usize) {
164 self.s = &self.s[c..];
165 self.position += c;
166 }
167
168 fn try_parse_double(&mut self) -> Option<f64> {
169 self.skip_whitespace();
170 if self.s.is_empty() {
171 return None;
172 }
173 let mut len = 0;
174 let mut dot_count = 0;
175 for (c, ch) in self.s.bytes().enumerate() {
176 if ch.is_ascii_digit() {
177 len = c + 1;
178 } else if ch == b'.' && dot_count == 0 {
179 len = c + 1;
180 dot_count += 1;
181 } else if ch == b'-' {
182 if len != 0 {
183 return None;
184 }
185 len = c + 1;
186 } else {
187 break;
188 }
189 }
190
191 let span = &self.s[..len];
192 if !span.bytes().any(|b| b.is_ascii_digit()) {
194 return None;
195 }
196 let res = span.parse::<f64>().ok()?;
197 self.advance(len);
198 Some(res)
199 }
200
201 fn is_eof_with_whitespace(&mut self) -> bool {
202 self.skip_whitespace();
203 self.s.is_empty()
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210 use crate::media::effects::EffectExtensions;
211
212 #[test]
213 fn parse_parses_blur() {
214 let effect = Effect::parse("blur(123.34)").unwrap();
215 let effect = effect.as_any().downcast_ref::<ImmutableBlurEffect>().unwrap();
216 assert_eq!(123.34, effect.radius());
217 }
218
219 const BLACK: u32 = 0xff000000;
220
221 #[test]
222 fn parse_parses_drop_shadow() {
223 for (s, x, y, r, color) in [
224 ("drop-shadow(10 20)", 10.0, 20.0, 0.0, BLACK),
225 ("drop-shadow( 10 20 ) ", 10.0, 20.0, 0.0, BLACK),
226 ("drop-shadow( 10 20 30 ) ", 10.0, 20.0, 30.0, BLACK),
227 ("drop-shadow(10 20 30)", 10.0, 20.0, 30.0, BLACK),
228 ("drop-shadow(-10 -20 30)", -10.0, -20.0, 30.0, BLACK),
229 ("drop-shadow(10 20 30 #ffff00ff)", 10.0, 20.0, 30.0, 0xffff00ff),
230 ("drop-shadow ( 10 20 30 #ffff00ff ) ", 10.0, 20.0, 30.0, 0xffff00ff),
231 ("drop-shadow(10 20 30 red)", 10.0, 20.0, 30.0, 0xffff0000),
232 ("drop-shadow ( 10 20 30 red ) ", 10.0, 20.0, 30.0, 0xffff0000),
233 ("drop-shadow(10 20 30 rgba(100, 30, 45, 90%))", 10.0, 20.0, 30.0, 0xe6641e2d),
234 ("drop-shadow(10 20 30 rgba(100, 30, 45, 90%) ) ", 10.0, 20.0, 30.0, 0xe6641e2d),
235 ] {
236 let effect = Effect::parse(s).unwrap_or_else(|e| panic!("{s}: {e}"));
237 let effect = effect.as_any().downcast_ref::<ImmutableDropShadowEffect>().unwrap();
238 assert_eq!(x, effect.offset_x(), "{s}");
239 assert_eq!(y, effect.offset_y(), "{s}");
240 assert_eq!(r, effect.blur_radius(), "{s}");
241 assert_eq!(1.0, effect.opacity(), "{s}");
242 assert_eq!(color, effect.color().to_uint32(), "{s}");
243 }
244 }
245
246 #[test]
247 fn invalid_effect_parse_fails() {
248 for b in ["blur", "blur(", "blur()", "blur(123", "blur(aaab)", "drop-shadow(-10 -20 -30)"] {
249 assert!(Effect::parse(b).is_err(), "{b}");
250 }
251 }
252
253 #[test]
254 fn padding_is_correctly_calculated() {
255 for (effect, left, top, right, bottom) in [
256 ("blur(2.5)", 4.0, 4.0, 4.0, 4.0),
257 ("blur(0)", 0.0, 0.0, 0.0, 0.0),
258 ("drop-shadow(10 15)", 0.0, 0.0, 10.0, 15.0),
259 ("drop-shadow(10 15 5)", 0.0, 0.0, 16.0, 21.0),
260 ("drop-shadow(0 0 5)", 6.0, 6.0, 6.0, 6.0),
261 ("drop-shadow(3 3 5)", 3.0, 3.0, 9.0, 9.0),
262 ] {
263 let parsed = Effect::parse(effect).unwrap();
264 let padding = EffectExtensions::get_effect_output_padding(Some(&*parsed));
265 assert_eq!(left, padding.left, "{effect}");
266 assert_eq!(top, padding.top, "{effect}");
267 assert_eq!(right, padding.right, "{effect}");
268 assert_eq!(bottom, padding.bottom, "{effect}");
269 }
270 }
271
272 #[test]
275 fn parse_rejects_lone_sign_and_trailing_garbage() {
276 for s in ["blur(-)", "blur(.)", "blur(1) x", "drop-shadow(1 2 3 red) x", "drop-shadow(1 2 3 nocolor)", ""] {
277 assert!(Effect::parse(s).is_err(), "{s}");
278 }
279 }
280}