Skip to main content

ferroui_base/media/
xaml_light.rs

1//! `XamlLight` of WinUI (`Microsoft.UI.Xaml.Media.XamlLight`): a light of the
2//! object model that an element holds in its `Lights`, which lights the
3//! elements and the brushes that target its id in the subtree of that
4//! element (winui-materials.md).
5
6use crate::collections::{FerroList, NotifyCollectionChangedEventArgs};
7use crate::media::{IBrush, XamlCompositionBrushBase};
8use crate::reactive::{Disposable, IDisposable};
9use crate::rendering::composition::{CompositionLight, CompositionVisual, DistantLight, PointLight, SpotLight};
10use crate::{ferro_class, FerroObject, FerroObjectImpl, Ref, Visual, WeakRef};
11use std::cell::RefCell;
12use std::rc::Rc;
13
14#[derive(Default)]
15struct XamlLightState {
16    /// `CompositionLight`: the composition light the subclass set.
17    composition_light: Option<Rc<CompositionLight>>,
18    /// The element whose `Lights` holds the light, while it is connected.
19    host: Option<WeakRef<Visual>>,
20}
21
22/// A light of the object model (WinUI `Microsoft.UI.Xaml.Media.XamlLight`).
23///
24/// A subclass gives the light an id ([`get_id`](XamlLightImpl::get_id)) and
25/// creates its composition light when the light is connected to the element
26/// that holds it ([`on_connected`](XamlLightImpl::on_connected)), setting it
27/// with [`set_composition_light`](Self::set_composition_light). The light
28/// then lights, in the subtree of that element:
29///
30/// - the elements that target its id ([`add_target_element`](Self::add_target_element)):
31///   their composition visuals are targets of the composition light, which
32///   lights their content;
33/// - the XAML brushes that target its id ([`add_target_brush`](Self::add_target_brush)):
34///   the scene lighting effects of those brushes receive the light.
35///
36/// The composition light is in the coordinates of the element that holds
37/// it (its `CoordinateSpace` is the composition visual of that element).
38#[repr(C)]
39pub struct XamlLight {
40    base: FerroObject,
41    state: RefCell<XamlLightState>,
42}
43
44ferro_class! {
45    XamlLight: FerroObject, virtuals XamlLightImpl: FerroObjectImpl {
46        /// `GetId`: the id the elements and brushes the light lights target.
47        fn get_id(this) -> String;
48
49        /// `OnConnected`: the light is in the `Lights` of `new_element`, which
50        /// is in a visual tree.
51        fn on_connected(this, new_element: &Visual);
52
53        /// `OnDisconnected`: the light left `old_element`, or the element
54        /// left its visual tree.
55        fn on_disconnected(this, old_element: &Visual);
56    }
57}
58crate::ferro_class_info!(XamlLight {
59    markup: {
60        namespace: "FerroUI.Media",
61    },
62});
63
64crate::ferro_impl_classes!(XamlLight: FerroObjectImpl);
65
66impl XamlLightImpl for XamlLight {
67    fn get_id(_this: &Self) -> String {
68        String::new()
69    }
70
71    fn on_connected(_this: &Self, _new_element: &Visual) {}
72
73    fn on_disconnected(_this: &Self, _old_element: &Visual) {}
74}
75
76impl XamlLight {
77    /// Creates the class data.
78    pub fn construct() -> Self {
79        Self { base: FerroObject::construct(), state: RefCell::new(XamlLightState::default()) }
80    }
81
82    /// The id of the light (`GetId`).
83    pub fn id(&self) -> String {
84        self.get_id()
85    }
86
87    /// `CompositionLight`.
88    pub fn composition_light(&self) -> Option<Rc<CompositionLight>> {
89        self.state.borrow().composition_light.clone()
90    }
91
92    /// Sets the composition light (`None` removes it). While the light is
93    /// connected, the composition light is bound to the element that holds
94    /// it: its coordinate space is that element, and it lights the targets
95    /// of the id of the light in its subtree.
96    pub fn set_composition_light(&self, light: Option<Rc<CompositionLight>>) {
97        let old = {
98            let mut state = self.state.borrow_mut();
99            let same = match (&state.composition_light, &light) {
100                (Some(a), Some(b)) => Rc::ptr_eq(a, b),
101                (None, None) => true,
102                _ => false,
103            };
104            if same {
105                return;
106            }
107            std::mem::replace(&mut state.composition_light, light)
108        };
109        if let Some(old) = old {
110            unbind(&old);
111        }
112        self.bind();
113    }
114
115    /// The element whose `Lights` holds the light, while it is connected.
116    pub fn host(&self) -> Option<Ref<Visual>> {
117        self.state.borrow().host.as_ref().and_then(WeakRef::upgrade)
118    }
119
120    /// Whether the light is connected to an element.
121    pub fn is_connected(&self) -> bool {
122        self.host().is_some()
123    }
124
125    fn connect(&self, host: &Visual) {
126        if self.host().is_some_and(|existing| std::ptr::eq::<Visual>(&*existing, host)) {
127            return;
128        }
129        if let Some(previous) = self.host() {
130            self.disconnect(&previous);
131        }
132        self.state.borrow_mut().host = Some(host.to_ref().downgrade());
133        self.on_connected(host);
134        self.bind();
135    }
136
137    fn disconnect(&self, host: &Visual) {
138        if !self.host().is_some_and(|existing| std::ptr::eq::<Visual>(&*existing, host)) {
139            return;
140        }
141        if let Some(light) = self.composition_light() {
142            unbind(&light);
143        }
144        self.on_disconnected(host);
145        self.state.borrow_mut().host = None;
146    }
147
148    /// Binds the composition light to the element that holds the light:
149    /// its XAML id and scope, its coordinate space, and the elements of the
150    /// subtree that target the id.
151    fn bind(&self) {
152        let (Some(light), Some(host)) = (self.composition_light(), self.host()) else { return };
153        let Some(scope) = host.composition_visual().map(|visual| (*visual).clone()) else { return };
154        let id = self.id();
155        light.set_xaml_binding(Some(id.clone()), Some(scope.clone()));
156        set_coordinate_space(&light, Some(scope));
157        let mut stack = vec![host.clone()];
158        while let Some(visual) = stack.pop() {
159            if visual.light_target_ids().iter().any(|existing| *existing == id) {
160                if let Some(target) = visual.composition_visual() {
161                    light.targets().add((*target).clone());
162                }
163            }
164            stack.extend(visual.visual_children().to_vec());
165        }
166    }
167
168    /// `AddTargetElement(id, element)`: the XAML lights of id `id` light
169    /// `element` (its content and its subtree) where it is in the subtree of
170    /// the element that holds them.
171    pub fn add_target_element(id: &str, element: &Visual) {
172        if element.add_light_target_id(id) {
173            offer_to_lights(element);
174        }
175    }
176
177    /// `RemoveTargetElement(id, element)`.
178    pub fn remove_target_element(id: &str, element: &Visual) {
179        if !element.remove_light_target_id(id) {
180            return;
181        }
182        let Some(target) = element.composition_visual().map(|visual| (*visual).clone()) else { return };
183        for light in lights_above(element) {
184            if light.id() == id {
185                if let Some(composition_light) = light.composition_light() {
186                    composition_light.targets().remove(&target);
187                }
188            }
189        }
190    }
191
192    /// `AddTargetBrush(id, brush)`: the XAML lights of id `id` light `brush`
193    /// where it is drawn in the subtree of the element that holds them. Only
194    /// a XAML composition brush can be lit (its scene lighting effects
195    /// receive the light); other brushes are left as they are.
196    pub fn add_target_brush(id: &str, brush: &Rc<dyn IBrush>) {
197        if let Some(brush) = XamlCompositionBrushBase::of(&**brush) {
198            brush.add_light_target_id(id);
199        }
200    }
201
202    /// `RemoveTargetBrush(id, brush)`.
203    pub fn remove_target_brush(id: &str, brush: &Rc<dyn IBrush>) {
204        if let Some(brush) = XamlCompositionBrushBase::of(&**brush) {
205            brush.remove_light_target_id(id);
206        }
207    }
208}
209
210fn set_coordinate_space(light: &Rc<CompositionLight>, space: Option<Rc<CompositionVisual>>) {
211    if let Some(spot) = SpotLight::from_light(light) {
212        spot.set_coordinate_space(space);
213    } else if let Some(point) = PointLight::from_light(light) {
214        point.set_coordinate_space(space);
215    } else if let Some(distant) = DistantLight::from_light(light) {
216        distant.set_coordinate_space(space);
217    }
218}
219
220fn unbind(light: &Rc<CompositionLight>) {
221    light.set_xaml_binding(None, None);
222    set_coordinate_space(light, None);
223    light.targets().remove_all();
224}
225
226/// The connected XAML lights of the element and its ancestors.
227fn lights_above(element: &Visual) -> Vec<Ref<XamlLight>> {
228    let mut lights = Vec::new();
229    let mut current = Some(element.to_ref());
230    while let Some(visual) = current {
231        if visual.has_lights() {
232            lights.extend(visual.lights().to_vec().into_iter().filter(|light| light.is_connected()));
233        }
234        current = visual.visual_parent();
235    }
236    lights
237}
238
239/// Adds `element` to the targets of the connected lights above it whose id
240/// it targets.
241fn offer_to_lights(element: &Visual) {
242    let Some(target) = element.composition_visual().map(|visual| (*visual).clone()) else { return };
243    let ids = element.light_target_ids();
244    for light in lights_above(element) {
245        if ids.iter().any(|id| *id == light.id()) {
246            if let Some(composition_light) = light.composition_light() {
247                composition_light.targets().add(target.clone());
248            }
249        }
250    }
251}
252
253/// Connects the lights of `lights` to `element` while it is in a visual
254/// tree, and follows the changes of the collection.
255pub(crate) fn track_lights(element: &Visual, lights: &FerroList<Ref<XamlLight>>) -> Vec<Rc<dyn IDisposable>> {
256    let connected: Rc<RefCell<Vec<Ref<XamlLight>>>> = Rc::new(RefCell::new(Vec::new()));
257    let sync = {
258        let weak = element.to_ref().downgrade();
259        let lights = lights.clone();
260        let connected = connected.clone();
261        // `attached`: whether the element is in a visual tree (`None`: as it says). The detached
262        // event is raised while the element still reports itself attached.
263        Rc::new(move |attached: Option<bool>| {
264            let Some(element) = weak.upgrade() else { return };
265            let attached = attached.unwrap_or_else(|| element.is_attached_to_visual_tree());
266            let wanted = if attached { lights.to_vec() } else { Vec::new() };
267            let previous = std::mem::take(&mut *connected.borrow_mut());
268            for light in previous.iter().filter(|light| !wanted.contains(light)) {
269                light.disconnect(&element);
270            }
271            for light in &wanted {
272                light.connect(&element);
273            }
274            *connected.borrow_mut() = wanted;
275        })
276    };
277    let token = {
278        let sync = sync.clone();
279        lights.add_collection_changed(Rc::new(move |_: &NotifyCollectionChangedEventArgs<'_, Ref<XamlLight>>| sync(None)))
280    };
281    let collection = {
282        let lights = lights.clone();
283        Disposable::create(move || {
284            lights.remove_collection_changed(token);
285        })
286    };
287    let attached = {
288        let sync = sync.clone();
289        element.attached_to_visual_tree(move |_| sync(Some(true)))
290    };
291    let detached = {
292        let sync = sync.clone();
293        element.detached_from_visual_tree(move |_| sync(Some(false)))
294    };
295    sync(None);
296    vec![collection, attached, detached]
297}
298
299/// Offers `element` to the lights above it each time it enters a visual
300/// tree.
301pub(crate) fn track_light_target(element: &Visual) -> Rc<dyn IDisposable> {
302    let weak = element.to_ref().downgrade();
303    element.attached_to_visual_tree(move |_| {
304        if let Some(element) = weak.upgrade() {
305            offer_to_lights(&element);
306        }
307    })
308}