1use crate::media::{BoxShadow, Color};
13use crate::rendering::composition::CompositionBrush;
14use crate::styling::ThemeVariant;
15use crate::{ferro_class, ferro_impl_classes, instantiate, FerroObject, FerroObjectImpl, Ref, Thickness, Visual, WeakRef};
16use std::cell::RefCell;
17use std::fmt;
18use std::rc::Rc;
19
20#[repr(C)]
23pub struct Shadow {
24 base: FerroObject,
25}
26
27ferro_class!(Shadow: FerroObject);
28ferro_impl_classes!(Shadow: FerroObjectImpl);
29
30impl Shadow {
31 pub fn construct() -> Self {
33 Self { base: FerroObject::construct() }
34 }
35
36 pub fn as_theme_shadow(&self) -> Option<&ThemeShadow> {
38 self.downcast_ref::<ThemeShadow>()
39 }
40}
41
42#[repr(C)]
59pub struct ThemeShadow {
60 base: Shadow,
61 receivers: UIElementWeakCollection,
62 mask: RefCell<Option<Rc<CompositionBrush>>>,
65}
66
67ferro_class!(ThemeShadow: Shadow);
68crate::ferro_class_info!(ThemeShadow { new: ThemeShadow::new });
69ferro_impl_classes!(ThemeShadow: FerroObjectImpl);
70
71impl ThemeShadow {
72 pub fn construct() -> Self {
74 Self {
75 base: Shadow::construct(),
76 receivers: UIElementWeakCollection::new(true),
77 mask: RefCell::new(None),
78 }
79 }
80
81 pub fn new() -> Ref<Self> {
82 instantiate(Self::construct())
83 }
84
85 pub fn is_drop_shadow_mode() -> bool {
89 true
90 }
91
92 pub fn receivers(&self) -> &UIElementWeakCollection {
96 &self.receivers
97 }
98
99 pub fn has_custom_receivers(&self) -> bool {
102 self.receivers.size() > 0
103 }
104
105 pub fn mask(&self) -> Option<Rc<CompositionBrush>> {
108 self.mask.borrow().clone()
109 }
110
111 pub fn set_mask(&self, value: Option<Rc<CompositionBrush>>) {
112 *self.mask.borrow_mut() = value;
113 }
114
115 pub(crate) fn add_owner(&self, owner: &Visual) {
119 self.receivers.owners.borrow_mut().push(owner.to_ref().downgrade());
120 }
121
122 pub(crate) fn remove_owner(&self, owner: &Visual) {
123 self.receivers.owners.borrow_mut().retain(|o| o.upgrade().is_some_and(|o| !std::ptr::eq(&*o, owner)));
124 }
125}
126
127#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129pub enum ThemeShadowError {
130 AncestorElementCannotBeShadowReceiver,
133 IndexOutOfRange,
135}
136
137impl fmt::Display for ThemeShadowError {
138 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139 match self {
140 ThemeShadowError::AncestorElementCannotBeShadowReceiver => {
141 f.write_str("an ancestor of an element that casts the shadow cannot receive it")
142 }
143 ThemeShadowError::IndexOutOfRange => f.write_str("the index is out of range"),
144 }
145 }
146}
147
148impl std::error::Error for ThemeShadowError {}
149
150pub struct UIElementWeakCollection {
155 items: RefCell<Vec<WeakRef<Visual>>>,
156 disallow_ancestors: bool,
159 owners: RefCell<Vec<WeakRef<Visual>>>,
161}
162
163impl UIElementWeakCollection {
164 fn new(disallow_ancestors: bool) -> Self {
165 Self { items: RefCell::new(Vec::new()), disallow_ancestors, owners: RefCell::new(Vec::new()) }
166 }
167
168 pub fn size(&self) -> usize {
170 self.items.borrow().len()
171 }
172
173 pub fn get_at(&self, index: usize) -> Option<Ref<Visual>> {
175 self.items.borrow().get(index).and_then(WeakRef::upgrade)
176 }
177
178 pub fn index_of(&self, element: &Visual) -> Option<usize> {
180 self.items.borrow().iter().position(|item| item.upgrade().is_some_and(|item| std::ptr::eq(&*item, element)))
181 }
182
183 pub fn contains(&self, element: &Visual) -> bool {
185 self.index_of(element).is_some()
186 }
187
188 pub fn append(&self, element: &Visual) -> Result<(), ThemeShadowError> {
190 self.validate(element)?;
191 self.items.borrow_mut().push(element.to_ref().downgrade());
192 Ok(())
193 }
194
195 pub fn insert_at(&self, index: usize, element: &Visual) -> Result<(), ThemeShadowError> {
197 if index > self.size() {
198 return Err(ThemeShadowError::IndexOutOfRange);
199 }
200 self.validate(element)?;
201 self.items.borrow_mut().insert(index, element.to_ref().downgrade());
202 Ok(())
203 }
204
205 pub fn set_at(&self, index: usize, element: &Visual) -> Result<(), ThemeShadowError> {
207 if index >= self.size() {
208 return Err(ThemeShadowError::IndexOutOfRange);
209 }
210 self.validate(element)?;
211 self.items.borrow_mut()[index] = element.to_ref().downgrade();
212 Ok(())
213 }
214
215 pub fn remove_at(&self, index: usize) -> Result<(), ThemeShadowError> {
217 if index >= self.size() {
218 return Err(ThemeShadowError::IndexOutOfRange);
219 }
220 self.items.borrow_mut().remove(index);
221 Ok(())
222 }
223
224 pub fn remove(&self, element: &Visual) -> bool {
226 match self.index_of(element) {
227 Some(index) => {
228 self.items.borrow_mut().remove(index);
229 true
230 }
231 None => false,
232 }
233 }
234
235 pub fn remove_at_end(&self) -> Result<(), ThemeShadowError> {
237 match self.items.borrow_mut().pop() {
238 Some(_) => Ok(()),
239 None => Err(ThemeShadowError::IndexOutOfRange),
240 }
241 }
242
243 pub fn clear(&self) {
245 self.items.borrow_mut().clear();
246 }
247
248 pub fn items(&self) -> Vec<Ref<Visual>> {
250 self.items.borrow().iter().filter_map(WeakRef::upgrade).collect()
251 }
252
253 pub fn replace_all(&self, elements: &[&Visual]) -> Result<(), ThemeShadowError> {
255 for element in elements {
256 self.validate(element)?;
257 }
258 *self.items.borrow_mut() = elements.iter().map(|element| element.to_ref().downgrade()).collect();
259 Ok(())
260 }
261
262 fn validate(&self, element: &Visual) -> Result<(), ThemeShadowError> {
267 if !self.disallow_ancestors {
268 return Ok(());
269 }
270 for owner in self.owners.borrow().iter().filter_map(WeakRef::upgrade) {
271 let mut current = Some(owner);
272 while let Some(ancestor) = current {
273 if std::ptr::eq(&*ancestor, element) {
274 return Err(ThemeShadowError::AncestorElementCannotBeShadowReceiver);
275 }
276 current = parent_following_popups(&ancestor);
277 }
278 }
279 Ok(())
280 }
281}
282
283fn parent_following_popups(element: &Visual) -> Option<Ref<Visual>> {
286 if let Some(parent) = element.visual_parent() {
287 return Some(parent);
288 }
289 element.parent().and_then(|parent| parent.downcast::<Visual>().ok())
290}
291
292#[derive(Clone, Copy, Debug, PartialEq, Eq)]
295pub enum ThemeShadowTheme {
296 Light,
297 Dark,
298 HighContrast,
300}
301
302impl ThemeShadowTheme {
303 pub fn of_variant(variant: Option<&ThemeVariant>) -> ThemeShadowTheme {
308 let mut current = variant.cloned();
309 let mut dark = false;
310 while let Some(variant) = current {
311 if variant.key().as_str() == Some("HighContrast") {
312 return ThemeShadowTheme::HighContrast;
313 }
314 if variant == ThemeVariant::dark() {
315 dark = true;
316 }
317 current = variant.inherit_variant();
318 }
319 if dark {
320 ThemeShadowTheme::Dark
321 } else {
322 ThemeShadowTheme::Light
323 }
324 }
325
326 pub fn of(element: &Visual) -> ThemeShadowTheme {
328 Self::of_variant(element.actual_theme_variant().as_ref())
329 }
330}
331
332#[derive(Clone, Copy, Debug, PartialEq)]
336pub struct DropShadowRecipe {
337 pub radius_x: f64,
339 pub radius_y: f64,
340 pub elevation: f32,
342 pub ambient_blur_radius: f32,
343 pub directional_blur_radius: f32,
344 pub ambient_y_offset: f32,
346 pub directional_y_offset: f32,
347 pub ambient_opacity: f32,
348 pub directional_opacity: f32,
349 pub ambient_color: Color,
351 pub directional_color: Color,
352 pub insets: Thickness,
354}
355
356impl DropShadowRecipe {
357 pub fn new(translation_z: f32, theme: ThemeShadowTheme) -> DropShadowRecipe {
369 let elevation = (translation_z / 2.0).min(64.0);
370 let mut ambient_blur_radius = 2.0f32;
371 let mut ambient_y_offset = 0.0f32;
372 let mut ambient_opacity = 0.0f32;
373 let mut directional_opacity = 0.0f32;
374
375 if elevation < 2.0 {
376 } else if elevation <= 16.0 {
378 directional_opacity = match theme {
380 ThemeShadowTheme::Light => (elevation / 100.0 + 0.06).min(0.14),
381 ThemeShadowTheme::Dark => 0.26,
382 ThemeShadowTheme::HighContrast => 0.0,
383 };
384 } else {
385 ambient_blur_radius = elevation / 3.0;
386 ambient_y_offset = 2.0;
387 (ambient_opacity, directional_opacity) = match theme {
388 ThemeShadowTheme::Light => (0.15, 0.19),
389 ThemeShadowTheme::Dark => (0.37, 0.37),
390 ThemeShadowTheme::HighContrast => (0.0, 0.0),
391 };
392 }
393
394 let black = |opacity: f32| Color::from_argb((opacity * 255.0) as u8, 0, 0, 0);
395 let directional_blur_radius = elevation;
396 let directional_y_offset = elevation * 0.5;
397 let max_blur_radius = ambient_blur_radius.max(directional_blur_radius).ceil() as f64;
398 let top = (ambient_blur_radius - ambient_y_offset).max(directional_blur_radius - directional_y_offset).ceil();
399 let bottom = (ambient_blur_radius + ambient_y_offset).max(directional_blur_radius + directional_y_offset).ceil();
400
401 DropShadowRecipe {
402 radius_x: 0.0,
403 radius_y: 0.0,
404 elevation,
405 ambient_blur_radius,
406 directional_blur_radius,
407 ambient_y_offset,
408 directional_y_offset,
409 ambient_opacity,
410 directional_opacity,
411 ambient_color: black(ambient_opacity),
412 directional_color: black(directional_opacity),
413 insets: Thickness::new(max_blur_radius, top as f64, max_blur_radius, bottom as f64),
414 }
415 }
416
417 pub fn with_corner_radius(mut self, radius_x: f64, radius_y: f64) -> DropShadowRecipe {
420 self.radius_x = radius_x.max(0.0);
421 self.radius_y = radius_y.max(0.0);
422 self
423 }
424
425 pub fn is_visible(&self) -> bool {
427 self.ambient_color.a > 0 || self.directional_color.a > 0
428 }
429
430 pub fn box_shadows(&self) -> Vec<BoxShadow> {
437 let mut shadows = Vec::with_capacity(2);
438 if self.ambient_color.a > 0 {
439 shadows.push(BoxShadow {
440 offset_x: 0.0,
441 offset_y: self.ambient_y_offset as f64,
442 blur: (self.ambient_blur_radius + 1.0) as f64,
443 spread: 0.0,
444 color: self.ambient_color,
445 is_inset: false,
446 });
447 }
448 if self.directional_color.a > 0 {
449 shadows.push(BoxShadow {
450 offset_x: 0.0,
451 offset_y: self.directional_y_offset as f64,
452 blur: (self.directional_blur_radius + 1.0) as f64,
453 spread: 0.0,
454 color: self.directional_color,
455 is_inset: false,
456 });
457 }
458 shadows
459 }
460}