azul_core/
window_state.rs

1use std::collections::{HashSet, BTreeMap};
2use crate::{
3    dom::{EventFilter, NotEventFilter, HoverEventFilter, FocusEventFilter, WindowEventFilter},
4    callbacks:: {CallbackInfo, CallbackType, HitTestItem, UpdateScreen},
5    id_tree::NodeId,
6    ui_state::UiState,
7    window::{
8        AcceleratorKey, FullWindowState, CallbacksOfHitTest, DetermineCallbackResult,
9    },
10};
11
12/// Determine which event / which callback(s) should be called and in which order
13///
14/// This function also updates / mutates the current window states `focused_node`
15/// as well as the `window_state.previous_state`
16pub fn determine_callbacks(
17    window_state: &mut FullWindowState,
18    hit_test_items: &[HitTestItem],
19    ui_state: &UiState,
20) -> CallbacksOfHitTest {
21
22    use std::collections::BTreeSet;
23
24    let mut needs_hover_redraw = false;
25    let mut needs_hover_relayout = false;
26    let mut nodes_with_callbacks: BTreeMap<NodeId, DetermineCallbackResult> = BTreeMap::new();
27
28    let current_window_events = get_window_events(window_state);
29    let current_hover_events = get_hover_events(&current_window_events);
30    let current_focus_events = get_focus_events(&current_hover_events);
31
32    let event_was_mouse_down    = current_window_events.contains(&WindowEventFilter::MouseDown);
33    let event_was_mouse_release = current_window_events.contains(&WindowEventFilter::MouseUp);
34    let event_was_mouse_enter   = current_window_events.contains(&WindowEventFilter::MouseEnter);
35    let event_was_mouse_leave   = current_window_events.contains(&WindowEventFilter::MouseLeave);
36
37    // Store the current window state so we can set it in this.previous_window_state later on
38    let mut previous_state = Box::new(window_state.clone());
39    previous_state.previous_window_state = None;
40
41    // TODO: If the current mouse is down, but the event
42    // wasn't a click, that means it was a drag
43
44    // Figure out what the hovered NodeIds are
45    let mut new_hit_node_ids: BTreeMap<NodeId, HitTestItem> = hit_test_items.iter().filter_map(|hit_test_item| {
46        ui_state.tag_ids_to_node_ids
47        .get(&hit_test_item.tag)
48        .map(|node_id| (*node_id, hit_test_item.clone()))
49    }).collect();
50
51    if event_was_mouse_leave {
52        new_hit_node_ids = BTreeMap::new();
53    }
54
55    // Figure out what the current focused NodeId is
56    if event_was_mouse_down || event_was_mouse_release {
57
58        // Find the first (closest to cursor in hierarchy) item that has a tabindex
59        let closest_focus_node = hit_test_items.iter().rev()
60        .find_map(|item| ui_state.tab_index_tags.get(&item.tag))
61        .cloned();
62
63        // Even if the focused node is None, we still have to update window_state.focused_node!
64        window_state.focused_node = closest_focus_node.map(|(node_id, _tab_idx)| (ui_state.dom_id.clone(), node_id));
65    }
66
67    macro_rules! insert_only_non_empty_callbacks {
68        ($node_id:expr, $hit_test_item:expr, $normal_hover_callbacks:expr) => ({
69            if !$normal_hover_callbacks.is_empty() {
70                let mut callback_result = nodes_with_callbacks.entry(*$node_id)
71                .or_insert_with(|| DetermineCallbackResult::default());
72
73                let item: Option<HitTestItem> = $hit_test_item;
74                if let Some(hit_test_item) = item {
75                    callback_result.hit_test_item = Some(hit_test_item);
76                }
77                callback_result.normal_callbacks.extend($normal_hover_callbacks.into_iter());
78            }
79        })
80    }
81
82    // Inserts the events from a given NodeId and an Option<HitTestItem> into the nodes_with_callbacks
83    macro_rules! insert_callbacks {(
84        $node_id:expr,
85        $hit_test_item:expr,
86        $hover_callbacks:ident,
87        $current_hover_events:ident,
88        $event_filter:ident
89    ) => ({
90            // BTreeMap<EventFilter, Callback>
91            let mut normal_hover_callbacks = BTreeMap::new();
92
93            // Insert all normal Hover events
94            if let Some(ui_state_hover_event_filters) = ui_state.$hover_callbacks.get($node_id) {
95                for current_hover_event in &$current_hover_events {
96                    if let Some(callback) = ui_state_hover_event_filters.get(current_hover_event) {
97                        normal_hover_callbacks.insert(EventFilter::$event_filter(*current_hover_event), callback.0);
98                    }
99                }
100            }
101
102            insert_only_non_empty_callbacks!($node_id, $hit_test_item, normal_hover_callbacks);
103        })
104    }
105
106    // Insert all normal window events
107    for (window_node_id, window_callbacks) in &ui_state.window_callbacks {
108        let normal_window_callbacks = window_callbacks.iter()
109            .filter(|(current_window_event, _)| current_window_events.contains(current_window_event))
110            .map(|(current_window_event, callback)| (EventFilter::Window(*current_window_event), callback.0))
111            .collect::<BTreeMap<_, _>>();
112        insert_only_non_empty_callbacks!(window_node_id, None, normal_window_callbacks);
113    }
114
115    // Insert (normal + default) hover events
116    for (hover_node_id, hit_test_item) in &new_hit_node_ids {
117        insert_callbacks!(hover_node_id, Some(hit_test_item.clone()), hover_callbacks, current_hover_events, Hover);
118    }
119
120    // Insert (normal + default) focus events
121    if let Some(current_focused_node) = &window_state.focused_node {
122        insert_callbacks!(&current_focused_node.1, None, focus_callbacks, current_focus_events, Focus);
123    }
124
125    // If the last focused node and the current focused node aren't the same,
126    // submit a FocusLost for the last node and a FocusReceived for the current one.
127    let mut focus_received_lost_events: BTreeMap<NodeId, FocusEventFilter> = BTreeMap::new();
128    match (window_state.focused_node.as_ref(), previous_state.focused_node.as_ref()) {
129        (Some((cur_dom_id, cur_node_id)), None) => {
130            if *cur_dom_id == ui_state.dom_id {
131                focus_received_lost_events.insert(*cur_node_id, FocusEventFilter::FocusReceived);
132            }
133        },
134        (None, Some((prev_dom_id, prev_node_id))) => {
135            if *prev_dom_id == ui_state.dom_id {
136                focus_received_lost_events.insert(*prev_node_id, FocusEventFilter::FocusLost);
137            }
138        },
139        (Some(cur), Some(prev)) => {
140            if *cur != *prev {
141                let (cur_dom_id, cur_node_id) = cur;
142                let (prev_dom_id, prev_node_id) = prev;
143                if *cur_dom_id == ui_state.dom_id {
144                    focus_received_lost_events.insert(*cur_node_id, FocusEventFilter::FocusReceived);
145                }
146                if *prev_dom_id == ui_state.dom_id {
147                    focus_received_lost_events.insert(*prev_node_id, FocusEventFilter::FocusLost);
148                }
149            }
150        }
151        (None, None) => { },
152    }
153
154    // Insert FocusReceived / FocusLost
155    for (node_id, focus_event) in &focus_received_lost_events {
156        let current_focus_leave_events = [focus_event.clone()];
157        insert_callbacks!(node_id, None, focus_callbacks, current_focus_leave_events, Focus);
158    }
159
160    let current_dom_id = ui_state.dom_id.clone();
161
162    macro_rules! mouse_enter {
163        ($node_id:expr, $hit_test_item:expr, $event_filter:ident) => ({
164
165            let node_is_focused = window_state.focused_node == Some((current_dom_id.clone(), $node_id));
166
167            // BTreeMap<EventFilter, Callback>
168            let mut normal_callbacks = BTreeMap::new();
169
170            // Insert all normal Hover(MouseEnter) events
171            if let Some(ui_state_hover_event_filters) = ui_state.hover_callbacks.get(&$node_id) {
172                if let Some(callback) = ui_state_hover_event_filters.get(&HoverEventFilter::$event_filter) {
173                    normal_callbacks.insert(EventFilter::Hover(HoverEventFilter::$event_filter), callback.0);
174                }
175            }
176
177            // Insert all normal Focus(MouseEnter) events
178            if node_is_focused {
179                if let Some(ui_state_focus_event_filters) = ui_state.focus_callbacks.get(&$node_id) {
180                    if let Some(callback) = ui_state_focus_event_filters.get(&FocusEventFilter::$event_filter) {
181                        normal_callbacks.insert(EventFilter::Focus(FocusEventFilter::$event_filter), callback.0);
182                    }
183                }
184            }
185
186            if !normal_callbacks.is_empty() {
187
188                let mut callback_result = nodes_with_callbacks.entry($node_id)
189                .or_insert_with(|| DetermineCallbackResult::default());
190
191                callback_result.hit_test_item = Some($hit_test_item);
192                callback_result.normal_callbacks.extend(normal_callbacks.into_iter());
193            }
194
195            if let Some((_, hover_group)) = ui_state.node_ids_to_tag_ids.get(&$node_id).and_then(|tag_for_this_node| {
196                ui_state.tag_ids_to_hover_active_states.get(&tag_for_this_node)
197            }) {
198                // We definitely need to redraw (on any :hover) change
199                needs_hover_redraw = true;
200                // Only set this to true if the :hover group actually affects the layout
201                if hover_group.affects_layout {
202                    needs_hover_relayout = true;
203                }
204            }
205        })
206    }
207
208    // Collect all On::MouseEnter nodes (for both hover and focus events)
209    let onmouseenter_nodes: BTreeMap<NodeId, HitTestItem> = new_hit_node_ids.iter()
210        .filter(|(current_node_id, _)| previous_state.hovered_nodes.get(&current_dom_id).and_then(|hn| hn.get(current_node_id)).is_none())
211        .map(|(x, y)| (*x, y.clone()))
212        .collect();
213
214    let onmouseenter_empty = onmouseenter_nodes.is_empty();
215
216    // Insert Focus(MouseEnter) and Hover(MouseEnter)
217    for (node_id, hit_test_item) in onmouseenter_nodes {
218        mouse_enter!(node_id, hit_test_item, MouseEnter);
219    }
220
221    // Collect all On::MouseLeave nodes (for both hover and focus events)
222    let onmouseleave_nodes: BTreeMap<NodeId, HitTestItem> = match previous_state.hovered_nodes.get(&current_dom_id) {
223        Some(hn) => {
224            hn.iter()
225            .filter(|(prev_node_id, _)| new_hit_node_ids.get(prev_node_id).is_none())
226            .map(|(x, y)| (*x, y.clone()))
227            .collect()
228        },
229        None => BTreeMap::new(),
230    };
231
232    let onmouseleave_empty = onmouseleave_nodes.is_empty();
233
234    // Insert Focus(MouseEnter) and Hover(MouseEnter)
235    for (node_id, hit_test_item) in onmouseleave_nodes {
236        mouse_enter!(node_id, hit_test_item, MouseLeave);
237    }
238
239    // If the mouse is down, but was up previously or vice versa, that means
240    // that a :hover or :active state may be invalidated. In that case we need
241    // to redraw the screen anyways. Setting relayout to true here in order to
242    let event_is_click_or_release = event_was_mouse_down || event_was_mouse_release;
243    if event_is_click_or_release || event_was_mouse_enter || event_was_mouse_leave || !onmouseenter_empty || !onmouseleave_empty {
244        needs_hover_redraw = true;
245        needs_hover_relayout = true;
246    }
247
248    // Insert all Not-callbacks, we need to filter out all Hover and Focus callbacks
249    // and then look at what callbacks were currently
250
251    // In order to create the Not Events we have to record which events were fired and on what nodes
252    // Then we need to go through the events and fire them if the event was present, but the NodeID was not
253    let mut reverse_event_hover_normal_list = BTreeMap::<HoverEventFilter, BTreeSet<NodeId>>::new();
254    let mut reverse_event_focus_normal_list = BTreeMap::<FocusEventFilter, BTreeSet<NodeId>>::new();
255
256    for (node_id, DetermineCallbackResult { normal_callbacks, .. }) in &nodes_with_callbacks {
257        for event_filter in normal_callbacks.keys() {
258            match event_filter {
259                EventFilter::Hover(h) => {
260                    reverse_event_hover_normal_list.entry(*h).or_insert_with(|| BTreeSet::new()).insert(*node_id);
261                },
262                EventFilter::Focus(f) => {
263                    reverse_event_focus_normal_list.entry(*f).or_insert_with(|| BTreeSet::new()).insert(*node_id);
264                },
265                _ => { },
266            }
267        }
268    }
269
270    // Insert NotEventFilter callbacks
271    for (node_id, not_event_filter_callback_list) in &ui_state.not_callbacks {
272        for (event_filter, event_callback) in not_event_filter_callback_list {
273            // If we have the event filter, but we don't have the NodeID, then insert the callback
274            match event_filter {
275                NotEventFilter::Hover(h) => {
276                    if let Some(on_node_ids) = reverse_event_hover_normal_list.get(&h) {
277                        if !on_node_ids.contains(node_id) {
278                            nodes_with_callbacks.entry(*node_id)
279                            .or_insert_with(|| DetermineCallbackResult::default())
280                            .normal_callbacks.insert(EventFilter::Not(*event_filter), event_callback.0);
281                        }
282                    }
283                },
284                NotEventFilter::Focus(_f) => {
285                    // TODO: Same thing for focus
286                }
287            }
288        }
289    }
290
291    window_state.hovered_nodes.insert(current_dom_id, new_hit_node_ids);
292    window_state.previous_window_state = Some(previous_state);
293
294    CallbacksOfHitTest {
295        needs_redraw_anyways: needs_hover_redraw,
296        needs_relayout_anyways: needs_hover_relayout,
297        nodes_with_callbacks,
298    }
299}
300
301pub fn get_window_events(window_state: &FullWindowState) -> HashSet<WindowEventFilter> {
302
303    use crate::window::CursorPosition::*;
304
305    let mut events_vec = HashSet::<WindowEventFilter>::new();
306
307    let previous_window_state = match &window_state.previous_window_state {
308        Some(s) => s,
309        None => return events_vec,
310    };
311
312    // mouse move events
313
314    match (previous_window_state.mouse_state.cursor_position, window_state.mouse_state.cursor_position) {
315        (InWindow(_), OutOfWindow) |
316        (InWindow(_), Uninitialized) => {
317            events_vec.insert(WindowEventFilter::MouseLeave);
318        },
319        (OutOfWindow, InWindow(_)) |
320        (Uninitialized, InWindow(_)) => {
321            events_vec.insert(WindowEventFilter::MouseEnter);
322        },
323        (InWindow(a), InWindow(b)) => {
324            if a != b {
325                events_vec.insert(WindowEventFilter::MouseOver);
326            }
327        },
328        _ => { },
329    }
330
331    // click events
332
333    if window_state.mouse_state.mouse_down() && !previous_window_state.mouse_state.mouse_down() {
334        events_vec.insert(WindowEventFilter::MouseDown);
335    }
336
337    if window_state.mouse_state.left_down && !previous_window_state.mouse_state.left_down {
338        events_vec.insert(WindowEventFilter::LeftMouseDown);
339    }
340
341    if window_state.mouse_state.right_down && !previous_window_state.mouse_state.right_down {
342        events_vec.insert(WindowEventFilter::RightMouseDown);
343    }
344
345    if window_state.mouse_state.middle_down && !previous_window_state.mouse_state.middle_down {
346        events_vec.insert(WindowEventFilter::MiddleMouseDown);
347    }
348
349    if previous_window_state.mouse_state.mouse_down() && !window_state.mouse_state.mouse_down() {
350        events_vec.insert(WindowEventFilter::MouseUp);
351    }
352
353    if previous_window_state.mouse_state.left_down && !window_state.mouse_state.left_down {
354        events_vec.insert(WindowEventFilter::LeftMouseUp);
355    }
356
357    if previous_window_state.mouse_state.right_down && !window_state.mouse_state.right_down {
358        events_vec.insert(WindowEventFilter::RightMouseUp);
359    }
360
361    if previous_window_state.mouse_state.middle_down && !window_state.mouse_state.middle_down {
362        events_vec.insert(WindowEventFilter::MiddleMouseUp);
363    }
364
365    // scroll events
366
367    let is_scroll_previous =
368        previous_window_state.mouse_state.scroll_x.is_some() ||
369        previous_window_state.mouse_state.scroll_y.is_some();
370
371    let is_scroll_now =
372        window_state.mouse_state.scroll_x.is_some() ||
373        window_state.mouse_state.scroll_y.is_some();
374
375    if !is_scroll_previous && is_scroll_now {
376        events_vec.insert(WindowEventFilter::ScrollStart);
377    }
378
379    if is_scroll_now {
380        events_vec.insert(WindowEventFilter::Scroll);
381    }
382
383    if is_scroll_previous && !is_scroll_now {
384        events_vec.insert(WindowEventFilter::ScrollEnd);
385    }
386
387    // keyboard events
388
389    if previous_window_state.keyboard_state.current_virtual_keycode.is_none() && window_state.keyboard_state.current_virtual_keycode.is_some() {
390        events_vec.insert(WindowEventFilter::VirtualKeyDown);
391    }
392
393    if window_state.keyboard_state.current_char.is_some() {
394        events_vec.insert(WindowEventFilter::TextInput);
395    }
396
397    if previous_window_state.keyboard_state.current_virtual_keycode.is_some() && window_state.keyboard_state.current_virtual_keycode.is_none() {
398        events_vec.insert(WindowEventFilter::VirtualKeyUp);
399    }
400
401    // misc events
402
403    if previous_window_state.hovered_file.is_none() && window_state.hovered_file.is_some() {
404        events_vec.insert(WindowEventFilter::HoveredFile);
405    }
406
407    if previous_window_state.hovered_file.is_some() && window_state.hovered_file.is_none() {
408        if window_state.dropped_file.is_some() {
409            events_vec.insert(WindowEventFilter::DroppedFile);
410        } else {
411            events_vec.insert(WindowEventFilter::HoveredFileCancelled);
412        }
413    }
414
415    events_vec
416}
417
418pub fn get_hover_events(input: &HashSet<WindowEventFilter>) -> HashSet<HoverEventFilter> {
419    input.iter().filter_map(|window_event| window_event.to_hover_event_filter()).collect()
420}
421
422pub fn get_focus_events(input: &HashSet<HoverEventFilter>) -> HashSet<FocusEventFilter> {
423    input.iter().filter_map(|hover_event| hover_event.to_focus_event_filter()).collect()
424}
425
426/// Utility function that, given the current keyboard state and a list of
427/// keyboard accelerators + callbacks, checks what callback can be invoked
428/// and the first matching callback. This leads to very readable
429/// (but still type checked) code like this:
430///
431/// ```no_run,ignore
432/// use azul::prelude::{AcceleratorKey::*, VirtualKeyCode::*};
433///
434/// fn my_Callback(info: CallbackInfo) -> UpdateScreen {
435///     keymap(info, &[
436///         [vec![Ctrl, S], save_document],
437///         [vec![Ctrl, N], create_new_document],
438///         [vec![Ctrl, O], open_new_file],
439///         [vec![Ctrl, Shift, N], create_new_window],
440///     ])
441/// }
442/// ```
443pub fn keymap<T>(
444    info: CallbackInfo,
445    events: &[(Vec<AcceleratorKey>, CallbackType)]
446) -> UpdateScreen {
447
448    let keyboard_state = info.get_keyboard_state().clone();
449
450    events
451        .iter()
452        .filter(|(keymap_character, _)| {
453            keymap_character
454                .iter()
455                .all(|keymap_char| keymap_char.matches(&keyboard_state))
456        })
457        .next()
458        .and_then(|(_, callback)| (callback)(info))
459}