azul_core/
ui_state.rs

1use std::{
2    fmt,
3    collections::BTreeMap,
4};
5use azul_css::CssProperty;
6use crate::{
7    FastHashMap,
8    id_tree::NodeId,
9    dom::{
10        Dom, CompactDom, DomId, TagId, TabIndex, DomString,
11        HoverEventFilter, FocusEventFilter, NotEventFilter,
12        WindowEventFilter,
13    },
14    callbacks::{
15        LayoutInfo, Callback, LayoutCallback,
16        IFrameCallback, RefAny,
17    },
18};
19#[cfg(feature = "opengl")]
20use crate::callbacks::GlCallback;
21
22pub struct UiState {
23    /// Unique identifier for the DOM
24    pub dom_id: DomId,
25    /// The actual DOM, rendered from the .layout() function
26    pub(crate) dom: CompactDom,
27    /// The style properties that should be overridden for this frame, cloned from the `Css`
28    pub dynamic_css_overrides: BTreeMap<NodeId, FastHashMap<DomString, CssProperty>>,
29    /// Stores all tags for nodes that need to activate on a `:hover` or `:active` event.
30    pub tag_ids_to_hover_active_states: BTreeMap<TagId, (NodeId, HoverGroup)>,
31
32    /// Tags -> Focusable nodes
33    pub tab_index_tags: BTreeMap<TagId, (NodeId, TabIndex)>,
34    /// Tags -> Draggable nodes
35    pub draggable_tags: BTreeMap<TagId, NodeId>,
36    /// Tag IDs -> Node IDs
37    pub tag_ids_to_node_ids: BTreeMap<TagId, NodeId>,
38    /// Reverse of `tag_ids_to_node_ids`.
39    pub node_ids_to_tag_ids: BTreeMap<NodeId, TagId>,
40
41    // For hover, focus and not callbacks, there needs to be a tag generated
42    // for hit-testing. Since window and desktop callbacks are not attached to
43    // any element, they only store the NodeId (where the event came from), but have
44    // no tag themselves.
45    pub hover_callbacks: BTreeMap<NodeId, BTreeMap<HoverEventFilter, (Callback, RefAny)>>,
46    pub focus_callbacks: BTreeMap<NodeId, BTreeMap<FocusEventFilter, (Callback, RefAny)>>,
47    pub not_callbacks: BTreeMap<NodeId, BTreeMap<NotEventFilter, (Callback, RefAny)>>,
48    pub window_callbacks: BTreeMap<NodeId, BTreeMap<WindowEventFilter, (Callback, RefAny)>>,
49}
50
51impl UiState {
52    #[inline(always)]
53    pub const fn get_dom(&self) -> &CompactDom {
54        &self.dom
55    }
56}
57
58impl fmt::Debug for UiState {
59    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
60        write!(f,
61            "UiState {{ \
62
63                dom: {:?}, \
64                dynamic_css_overrides: {:?}, \
65                tag_ids_to_hover_active_states: {:?}, \
66                tab_index_tags: {:?}, \
67                draggable_tags: {:?}, \
68                tag_ids_to_node_ids: {:?}, \
69                node_ids_to_tag_ids: {:?}, \
70                hover_callbacks: {:?}, \
71                focus_callbacks: {:?}, \
72                not_callbacks: {:?}, \
73                window_callbacks: {:?}, \
74            }}",
75
76            self.dom,
77            self.dynamic_css_overrides,
78            self.tag_ids_to_hover_active_states,
79            self.tab_index_tags,
80            self.draggable_tags,
81            self.tag_ids_to_node_ids,
82            self.node_ids_to_tag_ids,
83            self.hover_callbacks,
84            self.focus_callbacks,
85            self.not_callbacks,
86            self.window_callbacks,
87        )
88    }
89}
90
91/// In order to support :hover, the element must have a TagId, otherwise it
92/// will be disregarded in the hit-testing. A hover group
93#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
94pub struct HoverGroup {
95    /// Whether any property in the hover group will trigger a re-layout.
96    /// This is important for creating
97    pub affects_layout: bool,
98    /// Whether this path ends with `:active` or with `:hover`
99    pub active_or_hover: ActiveHover,
100}
101
102/// Sets whether an element needs to be selected for `:active` or for `:hover`
103#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
104pub enum ActiveHover {
105    Active,
106    Hover,
107}
108
109impl UiState {
110
111    /// The UiState contains all the tags (for hit-testing) as well as the mapping
112    /// from Hit-testing tags to NodeIds (which are important for filtering input events
113    /// and routing input events to the callbacks).
114    pub fn new(dom: Dom, parent_dom: Option<(DomId, NodeId)>) -> UiState {
115
116        let dom: CompactDom = dom.into();
117
118        // NOTE: Originally it was allowed to create a DOM with
119        // multiple root elements using `add_sibling()` and `with_sibling()`.
120        //
121        // However, it was decided to remove these functions (in commit #586933),
122        // as they aren't practical (you can achieve the same thing with one
123        // wrapper div and multiple add_child() calls) and they create problems
124        // when laying out elements since add_sibling() essentially modifies the
125        // space that the parent can distribute, which in code, simply looks weird
126        // and led to bugs.
127        //
128        // It is assumed that the DOM returned by the user has exactly one root node
129        // with no further siblings and that the root node is the Node with the ID 0.
130
131        // All tags that have can be focused (necessary for hit-testing)
132        let mut tab_index_tags = BTreeMap::new();
133        // All tags that have can be dragged & dropped (necessary for hit-testing)
134        let mut draggable_tags = BTreeMap::new();
135
136        // Mapping from tags to nodes (necessary so that the hit-testing can resolve the NodeId from any given tag)
137        let mut tag_ids_to_node_ids = BTreeMap::new();
138        // Mapping from nodes to tags, reverse mapping (not used right now, may be useful in the future)
139        let mut node_ids_to_tag_ids = BTreeMap::new();
140        // Which nodes have extra dynamic CSS overrides?
141        let mut dynamic_css_overrides = BTreeMap::new();
142
143        let mut hover_callbacks = BTreeMap::new();
144        let mut focus_callbacks = BTreeMap::new();
145        let mut not_callbacks = BTreeMap::new();
146        let mut window_callbacks = BTreeMap::new();
147
148        macro_rules! filter_step_0 {
149            ($event_filter:ident, $callback_type:ty, $data_source:expr, $filter_func:ident) => {{
150                let node_hover_callbacks: BTreeMap<$event_filter, $callback_type> = $data_source.iter()
151                .filter_map(|(event_filter, cb)| event_filter.$filter_func().map(|not_evt| (not_evt, cb.clone())))
152                .collect();
153                node_hover_callbacks
154            }};
155        };
156
157        macro_rules! filter_and_insert_callbacks {(
158                $node_id:ident,
159                $data_source:expr,
160                $event_filter:ident,
161                $callback_type:ty,
162                $filter_func:ident,
163                $final_callback_list:ident,
164        ) => {
165            let node_hover_callbacks = filter_step_0!($event_filter, $callback_type, $data_source, $filter_func);
166            if !node_hover_callbacks.is_empty() {
167                $final_callback_list.insert($node_id, node_hover_callbacks);
168            }
169        };(
170            $node_id:ident,
171            $data_source:expr,
172            $event_filter:ident,
173            $callback_type:ty,
174            $filter_func:ident,
175            $final_callback_list:ident,
176            $node_tag_id:ident,
177        ) => {
178            let node_hover_callbacks = filter_step_0!($event_filter, $callback_type, $data_source, $filter_func);
179            if !node_hover_callbacks.is_empty() {
180                $final_callback_list.insert($node_id, node_hover_callbacks);
181                let tag_id = $node_tag_id.unwrap_or_else(|| TagId::new());
182                $node_tag_id = Some(tag_id);
183            }
184        };}
185
186        TagId::reset();
187
188        {
189            let arena = &dom.arena;
190
191            debug_assert!(arena.node_hierarchy[NodeId::new(0)].next_sibling.is_none());
192
193            for node_id in arena.linear_iter() {
194
195                let node = &arena.node_data[node_id];
196
197                let mut node_tag_id = None;
198
199                // Optimization since on most nodes, the callbacks will be empty
200                if !node.get_callbacks().is_empty() {
201
202                    // Filter and insert HoverEventFilter callbacks
203                    filter_and_insert_callbacks!(
204                        node_id,
205                        node.get_callbacks(),
206                        HoverEventFilter,
207                        (Callback, RefAny),
208                        as_hover_event_filter,
209                        hover_callbacks,
210                        node_tag_id,
211                    );
212
213                    // Filter and insert FocusEventFilter callbacks
214                    filter_and_insert_callbacks!(
215                        node_id,
216                        node.get_callbacks(),
217                        FocusEventFilter,
218                        (Callback, RefAny),
219                        as_focus_event_filter,
220                        focus_callbacks,
221                        node_tag_id,
222                    );
223
224                    filter_and_insert_callbacks!(
225                        node_id,
226                        node.get_callbacks(),
227                        NotEventFilter,
228                        (Callback, RefAny),
229                        as_not_event_filter,
230                        not_callbacks,
231                        node_tag_id,
232                    );
233
234                    filter_and_insert_callbacks!(
235                        node_id,
236                        node.get_callbacks(),
237                        WindowEventFilter,
238                        (Callback, RefAny),
239                        as_window_event_filter,
240                        window_callbacks,
241                    );
242                }
243
244                if node.get_is_draggable() {
245                    let tag_id = node_tag_id.unwrap_or_else(|| TagId::new());
246                    draggable_tags.insert(tag_id, node_id);
247                    node_tag_id = Some(tag_id);
248                }
249
250                // It's a very common mistake is to set a default callback, but not to call
251                // .with_tab_index() - so this "fixes" this behaviour so that if at least one FocusEventFilter
252                // is set, the item automatically gets a tabindex attribute assigned.
253                let should_insert_tabindex_auto = !focus_callbacks.is_empty();
254                let node_tab_index = node.get_tab_index().or(if should_insert_tabindex_auto { Some(TabIndex::Auto) } else { None });
255
256                if let Some(tab_index) = node_tab_index {
257                    let tag_id = node_tag_id.unwrap_or_else(|| TagId::new());
258                    tab_index_tags.insert(tag_id, (node_id, tab_index));
259                    node_tag_id = Some(tag_id);
260                }
261
262                if let Some(tag_id) = node_tag_id {
263                    tag_ids_to_node_ids.insert(tag_id, node_id);
264                    node_ids_to_tag_ids.insert(node_id, tag_id);
265                }
266
267                // Collect all the styling overrides into one hash map
268                if !node.get_dynamic_css_overrides().is_empty() {
269                    dynamic_css_overrides.insert(node_id, node.get_dynamic_css_overrides().iter().cloned().collect());
270                }
271            }
272        }
273
274        UiState {
275            dom_id: DomId::new(parent_dom),
276            dom,
277            dynamic_css_overrides,
278            tag_ids_to_hover_active_states: BTreeMap::new(),
279
280            tab_index_tags,
281            draggable_tags,
282            node_ids_to_tag_ids,
283            tag_ids_to_node_ids,
284
285            hover_callbacks,
286            focus_callbacks,
287            not_callbacks,
288            window_callbacks,
289        }
290    }
291
292    pub fn new_from_app_state<'a>(
293        data: &RefAny,
294        layout_info: LayoutInfo<'a>,
295        parent_dom: Option<(DomId, NodeId)>,
296        layout_callback: LayoutCallback,
297    ) -> UiState {
298
299        use std::ffi::c_void;
300        use crate::callbacks::{LayoutInfoPtr, RefAnyPtr};
301
302        let data_box = Box::new(data.clone());
303        let layout_info_box = Box::new(layout_info);
304        let data_box_ptr = Box::into_raw(data_box) as *mut c_void;
305        let layout_info_box_ptr = Box::into_raw(layout_info_box) as *mut c_void;
306
307        let dom_ptr = (layout_callback)(
308            RefAnyPtr { ptr: data_box_ptr },
309            LayoutInfoPtr { ptr: layout_info_box_ptr }
310        );
311
312        let dom = unsafe { Box::<Dom>::from_raw(dom_ptr.ptr as *mut Dom) };
313        let _ = unsafe { Box::<RefAny>::from_raw(data_box_ptr as *mut RefAny) };
314        let _ = unsafe { Box::<LayoutInfo<'a>>::from_raw(layout_info_box_ptr as *mut LayoutInfo<'a>) };
315
316        Self::new(*dom, parent_dom)
317    }
318
319    pub fn create_tags_for_hover_nodes(&mut self, hover_nodes: &BTreeMap<NodeId, HoverGroup>) {
320
321        for (hover_node_id, hover_group) in hover_nodes {
322            let hover_tag = match self.node_ids_to_tag_ids.get(hover_node_id) {
323                Some(tag_id) => *tag_id,
324                None => TagId::new(),
325            };
326
327            self.node_ids_to_tag_ids.insert(*hover_node_id, hover_tag);
328            self.tag_ids_to_node_ids.insert(hover_tag, *hover_node_id);
329            self.tag_ids_to_hover_active_states.insert(hover_tag, (*hover_node_id, *hover_group));
330        }
331    }
332
333    pub fn scan_for_iframe_callbacks(&self) -> Vec<(NodeId, &(IFrameCallback, RefAny))> {
334        use crate::dom::NodeType::IFrame;
335        self.dom.arena.node_hierarchy.linear_iter().filter_map(|node_id| {
336            let node_data = &self.dom.arena.node_data[node_id];
337            match node_data.get_node_type() {
338                IFrame(cb) => Some((node_id, cb)),
339                _ => None,
340            }
341        }).collect()
342    }
343
344    #[cfg(feature = "opengl")]
345    pub fn scan_for_gltexture_callbacks(&self) -> Vec<(NodeId, &(GlCallback, RefAny))> {
346        use crate::dom::NodeType::GlTexture;
347        self.dom.arena.node_hierarchy.linear_iter().filter_map(|node_id| {
348            let node_data = &self.dom.arena.node_data[node_id];
349            match node_data.get_node_type() {
350                GlTexture(cb) => Some((node_id, cb)),
351                _ => None,
352            }
353        }).collect()
354    }
355}