azul_core/
ui_state.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
use std::{
    fmt,
    collections::BTreeMap,
};
use azul_css::CssProperty;
use crate::{
    FastHashMap,
    id_tree::NodeId,
    dom::{
        Dom, CompactDom, DomId, TagId, TabIndex, DomString,
        HoverEventFilter, FocusEventFilter, NotEventFilter,
        WindowEventFilter,
    },
    callbacks::{
        LayoutInfo, Callback, LayoutCallback,
        IFrameCallback, RefAny,
    },
};
#[cfg(feature = "opengl")]
use crate::callbacks::GlCallback;

pub struct UiState {
    /// Unique identifier for the DOM
    pub dom_id: DomId,
    /// The actual DOM, rendered from the .layout() function
    pub(crate) dom: CompactDom,
    /// The style properties that should be overridden for this frame, cloned from the `Css`
    pub dynamic_css_overrides: BTreeMap<NodeId, FastHashMap<DomString, CssProperty>>,
    /// Stores all tags for nodes that need to activate on a `:hover` or `:active` event.
    pub tag_ids_to_hover_active_states: BTreeMap<TagId, (NodeId, HoverGroup)>,

    /// Tags -> Focusable nodes
    pub tab_index_tags: BTreeMap<TagId, (NodeId, TabIndex)>,
    /// Tags -> Draggable nodes
    pub draggable_tags: BTreeMap<TagId, NodeId>,
    /// Tag IDs -> Node IDs
    pub tag_ids_to_node_ids: BTreeMap<TagId, NodeId>,
    /// Reverse of `tag_ids_to_node_ids`.
    pub node_ids_to_tag_ids: BTreeMap<NodeId, TagId>,

    // For hover, focus and not callbacks, there needs to be a tag generated
    // for hit-testing. Since window and desktop callbacks are not attached to
    // any element, they only store the NodeId (where the event came from), but have
    // no tag themselves.
    pub hover_callbacks: BTreeMap<NodeId, BTreeMap<HoverEventFilter, (Callback, RefAny)>>,
    pub focus_callbacks: BTreeMap<NodeId, BTreeMap<FocusEventFilter, (Callback, RefAny)>>,
    pub not_callbacks: BTreeMap<NodeId, BTreeMap<NotEventFilter, (Callback, RefAny)>>,
    pub window_callbacks: BTreeMap<NodeId, BTreeMap<WindowEventFilter, (Callback, RefAny)>>,
}

impl UiState {
    #[inline(always)]
    pub const fn get_dom(&self) -> &CompactDom {
        &self.dom
    }
}

impl fmt::Debug for UiState {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,
            "UiState {{ \

                dom: {:?}, \
                dynamic_css_overrides: {:?}, \
                tag_ids_to_hover_active_states: {:?}, \
                tab_index_tags: {:?}, \
                draggable_tags: {:?}, \
                tag_ids_to_node_ids: {:?}, \
                node_ids_to_tag_ids: {:?}, \
                hover_callbacks: {:?}, \
                focus_callbacks: {:?}, \
                not_callbacks: {:?}, \
                window_callbacks: {:?}, \
            }}",

            self.dom,
            self.dynamic_css_overrides,
            self.tag_ids_to_hover_active_states,
            self.tab_index_tags,
            self.draggable_tags,
            self.tag_ids_to_node_ids,
            self.node_ids_to_tag_ids,
            self.hover_callbacks,
            self.focus_callbacks,
            self.not_callbacks,
            self.window_callbacks,
        )
    }
}

/// In order to support :hover, the element must have a TagId, otherwise it
/// will be disregarded in the hit-testing. A hover group
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd)]
pub struct HoverGroup {
    /// Whether any property in the hover group will trigger a re-layout.
    /// This is important for creating
    pub affects_layout: bool,
    /// Whether this path ends with `:active` or with `:hover`
    pub active_or_hover: ActiveHover,
}

/// Sets whether an element needs to be selected for `:active` or for `:hover`
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
pub enum ActiveHover {
    Active,
    Hover,
}

impl UiState {

    /// The UiState contains all the tags (for hit-testing) as well as the mapping
    /// from Hit-testing tags to NodeIds (which are important for filtering input events
    /// and routing input events to the callbacks).
    pub fn new(dom: Dom, parent_dom: Option<(DomId, NodeId)>) -> UiState {

        let dom: CompactDom = dom.into();

        // NOTE: Originally it was allowed to create a DOM with
        // multiple root elements using `add_sibling()` and `with_sibling()`.
        //
        // However, it was decided to remove these functions (in commit #586933),
        // as they aren't practical (you can achieve the same thing with one
        // wrapper div and multiple add_child() calls) and they create problems
        // when laying out elements since add_sibling() essentially modifies the
        // space that the parent can distribute, which in code, simply looks weird
        // and led to bugs.
        //
        // It is assumed that the DOM returned by the user has exactly one root node
        // with no further siblings and that the root node is the Node with the ID 0.

        // All tags that have can be focused (necessary for hit-testing)
        let mut tab_index_tags = BTreeMap::new();
        // All tags that have can be dragged & dropped (necessary for hit-testing)
        let mut draggable_tags = BTreeMap::new();

        // Mapping from tags to nodes (necessary so that the hit-testing can resolve the NodeId from any given tag)
        let mut tag_ids_to_node_ids = BTreeMap::new();
        // Mapping from nodes to tags, reverse mapping (not used right now, may be useful in the future)
        let mut node_ids_to_tag_ids = BTreeMap::new();
        // Which nodes have extra dynamic CSS overrides?
        let mut dynamic_css_overrides = BTreeMap::new();

        let mut hover_callbacks = BTreeMap::new();
        let mut focus_callbacks = BTreeMap::new();
        let mut not_callbacks = BTreeMap::new();
        let mut window_callbacks = BTreeMap::new();

        macro_rules! filter_step_0 {
            ($event_filter:ident, $callback_type:ty, $data_source:expr, $filter_func:ident) => {{
                let node_hover_callbacks: BTreeMap<$event_filter, $callback_type> = $data_source.iter()
                .filter_map(|(event_filter, cb)| event_filter.$filter_func().map(|not_evt| (not_evt, cb.clone())))
                .collect();
                node_hover_callbacks
            }};
        };

        macro_rules! filter_and_insert_callbacks {(
                $node_id:ident,
                $data_source:expr,
                $event_filter:ident,
                $callback_type:ty,
                $filter_func:ident,
                $final_callback_list:ident,
        ) => {
            let node_hover_callbacks = filter_step_0!($event_filter, $callback_type, $data_source, $filter_func);
            if !node_hover_callbacks.is_empty() {
                $final_callback_list.insert($node_id, node_hover_callbacks);
            }
        };(
            $node_id:ident,
            $data_source:expr,
            $event_filter:ident,
            $callback_type:ty,
            $filter_func:ident,
            $final_callback_list:ident,
            $node_tag_id:ident,
        ) => {
            let node_hover_callbacks = filter_step_0!($event_filter, $callback_type, $data_source, $filter_func);
            if !node_hover_callbacks.is_empty() {
                $final_callback_list.insert($node_id, node_hover_callbacks);
                let tag_id = $node_tag_id.unwrap_or_else(|| TagId::new());
                $node_tag_id = Some(tag_id);
            }
        };}

        TagId::reset();

        {
            let arena = &dom.arena;

            debug_assert!(arena.node_hierarchy[NodeId::new(0)].next_sibling.is_none());

            for node_id in arena.linear_iter() {

                let node = &arena.node_data[node_id];

                let mut node_tag_id = None;

                // Optimization since on most nodes, the callbacks will be empty
                if !node.get_callbacks().is_empty() {

                    // Filter and insert HoverEventFilter callbacks
                    filter_and_insert_callbacks!(
                        node_id,
                        node.get_callbacks(),
                        HoverEventFilter,
                        (Callback, RefAny),
                        as_hover_event_filter,
                        hover_callbacks,
                        node_tag_id,
                    );

                    // Filter and insert FocusEventFilter callbacks
                    filter_and_insert_callbacks!(
                        node_id,
                        node.get_callbacks(),
                        FocusEventFilter,
                        (Callback, RefAny),
                        as_focus_event_filter,
                        focus_callbacks,
                        node_tag_id,
                    );

                    filter_and_insert_callbacks!(
                        node_id,
                        node.get_callbacks(),
                        NotEventFilter,
                        (Callback, RefAny),
                        as_not_event_filter,
                        not_callbacks,
                        node_tag_id,
                    );

                    filter_and_insert_callbacks!(
                        node_id,
                        node.get_callbacks(),
                        WindowEventFilter,
                        (Callback, RefAny),
                        as_window_event_filter,
                        window_callbacks,
                    );
                }

                if node.get_is_draggable() {
                    let tag_id = node_tag_id.unwrap_or_else(|| TagId::new());
                    draggable_tags.insert(tag_id, node_id);
                    node_tag_id = Some(tag_id);
                }

                // It's a very common mistake is to set a default callback, but not to call
                // .with_tab_index() - so this "fixes" this behaviour so that if at least one FocusEventFilter
                // is set, the item automatically gets a tabindex attribute assigned.
                let should_insert_tabindex_auto = !focus_callbacks.is_empty();
                let node_tab_index = node.get_tab_index().or(if should_insert_tabindex_auto { Some(TabIndex::Auto) } else { None });

                if let Some(tab_index) = node_tab_index {
                    let tag_id = node_tag_id.unwrap_or_else(|| TagId::new());
                    tab_index_tags.insert(tag_id, (node_id, tab_index));
                    node_tag_id = Some(tag_id);
                }

                if let Some(tag_id) = node_tag_id {
                    tag_ids_to_node_ids.insert(tag_id, node_id);
                    node_ids_to_tag_ids.insert(node_id, tag_id);
                }

                // Collect all the styling overrides into one hash map
                if !node.get_dynamic_css_overrides().is_empty() {
                    dynamic_css_overrides.insert(node_id, node.get_dynamic_css_overrides().iter().cloned().collect());
                }
            }
        }

        UiState {
            dom_id: DomId::new(parent_dom),
            dom,
            dynamic_css_overrides,
            tag_ids_to_hover_active_states: BTreeMap::new(),

            tab_index_tags,
            draggable_tags,
            node_ids_to_tag_ids,
            tag_ids_to_node_ids,

            hover_callbacks,
            focus_callbacks,
            not_callbacks,
            window_callbacks,
        }
    }

    pub fn new_from_app_state<'a>(
        data: &RefAny,
        layout_info: LayoutInfo<'a>,
        parent_dom: Option<(DomId, NodeId)>,
        layout_callback: LayoutCallback,
    ) -> UiState {

        use std::ffi::c_void;
        use crate::callbacks::{LayoutInfoPtr, RefAnyPtr};

        let data_box = Box::new(data.clone());
        let layout_info_box = Box::new(layout_info);
        let data_box_ptr = Box::into_raw(data_box) as *mut c_void;
        let layout_info_box_ptr = Box::into_raw(layout_info_box) as *mut c_void;

        let dom_ptr = (layout_callback)(
            RefAnyPtr { ptr: data_box_ptr },
            LayoutInfoPtr { ptr: layout_info_box_ptr }
        );

        let dom = unsafe { Box::<Dom>::from_raw(dom_ptr.ptr as *mut Dom) };
        let _ = unsafe { Box::<RefAny>::from_raw(data_box_ptr as *mut RefAny) };
        let _ = unsafe { Box::<LayoutInfo<'a>>::from_raw(layout_info_box_ptr as *mut LayoutInfo<'a>) };

        Self::new(*dom, parent_dom)
    }

    pub fn create_tags_for_hover_nodes(&mut self, hover_nodes: &BTreeMap<NodeId, HoverGroup>) {

        for (hover_node_id, hover_group) in hover_nodes {
            let hover_tag = match self.node_ids_to_tag_ids.get(hover_node_id) {
                Some(tag_id) => *tag_id,
                None => TagId::new(),
            };

            self.node_ids_to_tag_ids.insert(*hover_node_id, hover_tag);
            self.tag_ids_to_node_ids.insert(hover_tag, *hover_node_id);
            self.tag_ids_to_hover_active_states.insert(hover_tag, (*hover_node_id, *hover_group));
        }
    }

    pub fn scan_for_iframe_callbacks(&self) -> Vec<(NodeId, &(IFrameCallback, RefAny))> {
        use crate::dom::NodeType::IFrame;
        self.dom.arena.node_hierarchy.linear_iter().filter_map(|node_id| {
            let node_data = &self.dom.arena.node_data[node_id];
            match node_data.get_node_type() {
                IFrame(cb) => Some((node_id, cb)),
                _ => None,
            }
        }).collect()
    }

    #[cfg(feature = "opengl")]
    pub fn scan_for_gltexture_callbacks(&self) -> Vec<(NodeId, &(GlCallback, RefAny))> {
        use crate::dom::NodeType::GlTexture;
        self.dom.arena.node_hierarchy.linear_iter().filter_map(|node_id| {
            let node_data = &self.dom.arena.node_data[node_id];
            match node_data.get_node_type() {
                GlTexture(cb) => Some((node_id, cb)),
                _ => None,
            }
        }).collect()
    }
}