Skip to content

Commit 9060de0

Browse files
authored
Add extra mouse event handling (#304)
* working contextmenu and dblclick events * working mouse enter/leave/over/out * working onscroll and onwheel * remove click_test * fix redraw on wheel and use node chain for enter/leave * fix range issues with node diffing * fmt/clippy and remove unused enter_stack * remove unnecessary cast * remove extra request_redraw
1 parent 6d8c6d0 commit 9060de0

File tree

8 files changed

+429
-37
lines changed

8 files changed

+429
-37
lines changed

packages/blitz-dom/src/document.rs

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ use crate::{
1616
EventDriver, HtmlParserProvider, Node, NodeData, NoopEventHandler, TextNodeData,
1717
};
1818
use blitz_traits::devtools::DevtoolSettings;
19-
use blitz_traits::events::{DomEvent, HitResult, UiEvent};
19+
use blitz_traits::events::{BlitzScrollEvent, DomEvent, DomEventData, HitResult, UiEvent};
2020
use blitz_traits::navigation::{DummyNavigationProvider, NavigationProvider};
2121
use blitz_traits::net::{DummyNetProvider, NetProvider, Request};
2222
use blitz_traits::shell::{ColorScheme, DummyShellProvider, ShellProvider, Viewport};
@@ -34,6 +34,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
3434
use std::sync::mpsc::{Receiver, Sender, channel};
3535
use std::sync::{Arc, Mutex};
3636
use std::task::Context as TaskContext;
37+
use std::time::Instant;
3738
use style::Atom;
3839
use style::animation::DocumentAnimationSet;
3940
use style::attr::{AttrIdentifier, AttrValue};
@@ -156,6 +157,10 @@ pub struct BaseDocument {
156157
pub(crate) active_node_id: Option<usize>,
157158
/// The node which recieved a mousedown event (if any)
158159
pub(crate) mousedown_node_id: Option<usize>,
160+
/// The last time a click was made
161+
pub(crate) last_click_time: Option<Instant>,
162+
/// How many clicks have been made in quick succession
163+
pub(crate) click_count: u16,
159164

160165
// TODO: collapse animating state into a bitflags
161166
/// Whether there are active CSS animations/transitions (so we should re-render every frame)
@@ -315,6 +320,8 @@ impl BaseDocument {
315320
navigation_provider,
316321
shell_provider,
317322
html_parser_provider,
323+
last_click_time: None,
324+
click_count: 0,
318325
};
319326

320327
// Initialise document with root Document node
@@ -1129,14 +1136,26 @@ impl BaseDocument {
11291136
Some(CursorIcon::Default)
11301137
}
11311138

1132-
pub fn scroll_node_by(&mut self, node_id: usize, x: f64, y: f64) {
1133-
self.scroll_node_by_has_changed(node_id, x, y);
1139+
pub fn scroll_node_by<F: FnMut(DomEvent)>(
1140+
&mut self,
1141+
node_id: usize,
1142+
x: f64,
1143+
y: f64,
1144+
dispatch_event: F,
1145+
) {
1146+
self.scroll_node_by_has_changed(node_id, x, y, dispatch_event);
11341147
}
11351148

11361149
/// Scroll a node by given x and y
11371150
/// Will bubble scrolling up to parent node once it can no longer scroll further
11381151
/// If we're already at the root node, bubbles scrolling up to the viewport
1139-
pub fn scroll_node_by_has_changed(&mut self, node_id: usize, x: f64, y: f64) -> bool {
1152+
pub fn scroll_node_by_has_changed<F: FnMut(DomEvent)>(
1153+
&mut self,
1154+
node_id: usize,
1155+
x: f64,
1156+
y: f64,
1157+
mut dispatch_event: F,
1158+
) -> bool {
11401159
let Some(node) = self.nodes.get_mut(node_id) else {
11411160
return false;
11421161
};
@@ -1170,7 +1189,7 @@ impl BaseDocument {
11701189
// Handle sub document case
11711190
if let Some(sub_doc) = node.subdoc_mut() {
11721191
let has_changed = if let Some(hover_node_id) = sub_doc.get_hover_node_id() {
1173-
sub_doc.scroll_node_by_has_changed(hover_node_id, x, y)
1192+
sub_doc.scroll_node_by_has_changed(hover_node_id, x, y, dispatch_event)
11741193
} else {
11751194
sub_doc.scroll_viewport_by_has_changed(x, y)
11761195
};
@@ -1206,9 +1225,24 @@ impl BaseDocument {
12061225

12071226
let has_changed = node.scroll_offset != initial;
12081227

1228+
if has_changed {
1229+
let layout = node.final_layout;
1230+
let event = BlitzScrollEvent {
1231+
scroll_top: node.scroll_offset.y,
1232+
scroll_left: node.scroll_offset.x,
1233+
scroll_width: layout.scroll_width() as i32,
1234+
scroll_height: layout.scroll_height() as i32,
1235+
client_width: layout.size.width as i32,
1236+
client_height: layout.size.height as i32,
1237+
};
1238+
1239+
dispatch_event(DomEvent::new(node_id, DomEventData::Scroll(event)));
1240+
}
1241+
12091242
if bubble_x != 0.0 || bubble_y != 0.0 {
12101243
if let Some(parent) = node.parent {
1211-
return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y) | has_changed;
1244+
return self.scroll_node_by_has_changed(parent, bubble_x, bubble_y, dispatch_event)
1245+
| has_changed;
12121246
} else {
12131247
return self.scroll_viewport_by_has_changed(bubble_x, bubble_y) | has_changed;
12141248
}

packages/blitz-dom/src/events/driver.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,69 @@ impl<'doc, Handler: EventHandler> EventDriver<'doc, Handler> {
5555
UiEvent::MouseMove(event) => {
5656
let dom_x = event.x + viewport_scroll.x as f32 / zoom;
5757
let dom_y = event.y + viewport_scroll.y as f32 / zoom;
58-
self.doc_mut().set_hover_to(dom_x, dom_y);
58+
let changed = self.doc_mut().set_hover_to(dom_x, dom_y);
59+
60+
let prev_hover_node_id = hover_node_id;
5961
hover_node_id = self.doc().hover_node_id;
62+
63+
if changed {
64+
let mut old_chain = prev_hover_node_id
65+
.map(|id| self.doc().node_chain(id))
66+
.unwrap_or_default();
67+
let mut new_chain = hover_node_id
68+
.map(|id| self.doc().node_chain(id))
69+
.unwrap_or_default();
70+
old_chain.reverse();
71+
new_chain.reverse();
72+
73+
// Find the difference in the node chain of the last hovered objected and the newest
74+
let old_len = old_chain.len();
75+
let new_len = new_chain.len();
76+
77+
let first_difference_index = old_chain
78+
.iter()
79+
.zip(&new_chain)
80+
.position(|(old, new)| old != new)
81+
.unwrap_or_else(|| old_len.min(new_len));
82+
83+
if let Some(target) = prev_hover_node_id {
84+
self.handle_dom_event(DomEvent::new(
85+
target,
86+
DomEventData::MouseOut(event.clone()),
87+
));
88+
89+
// Send an mouseleave event to all old elements on the chain
90+
for node_id in old_chain
91+
.get(first_difference_index..)
92+
.unwrap_or(&[])
93+
.iter()
94+
{
95+
self.handle_dom_event(DomEvent::new(
96+
*node_id,
97+
DomEventData::MouseLeave(event.clone()),
98+
));
99+
}
100+
}
101+
102+
if let Some(target) = hover_node_id {
103+
self.handle_dom_event(DomEvent::new(
104+
target,
105+
DomEventData::MouseOver(event.clone()),
106+
));
107+
108+
// Send an mouseenter event to all new elements on the chain
109+
for node_id in new_chain
110+
.get(first_difference_index..)
111+
.unwrap_or(&[])
112+
.iter()
113+
{
114+
self.handle_dom_event(DomEvent::new(
115+
*node_id,
116+
DomEventData::MouseEnter(event.clone()),
117+
));
118+
}
119+
}
120+
}
60121
}
61122
UiEvent::MouseDown(_) => {
62123
self.doc_mut().active_node();
@@ -72,6 +133,7 @@ impl<'doc, Handler: EventHandler> EventDriver<'doc, Handler> {
72133
UiEvent::MouseMove(_) => hover_node_id,
73134
UiEvent::MouseUp(_) => hover_node_id,
74135
UiEvent::MouseDown(_) => hover_node_id,
136+
UiEvent::Wheel(_) => hover_node_id,
75137
UiEvent::KeyUp(_) => focussed_node_id,
76138
UiEvent::KeyDown(_) => focussed_node_id,
77139
UiEvent::Ime(_) => focussed_node_id,
@@ -93,6 +155,7 @@ impl<'doc, Handler: EventHandler> EventDriver<'doc, Handler> {
93155
y: data.y + viewport_scroll.y as f32 / zoom,
94156
..data
95157
}),
158+
UiEvent::Wheel(data) => DomEventData::Wheel(data),
96159
UiEvent::KeyUp(data) => DomEventData::KeyUp(data),
97160
UiEvent::KeyDown(data) => DomEventData::KeyDown(data),
98161
UiEvent::Ime(data) => DomEventData::Ime(data),

packages/blitz-dom/src/events/mod.rs

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub(crate) use keyboard::handle_keypress;
1010
use mouse::handle_mouseup;
1111
pub(crate) use mouse::{handle_click, handle_mousedown, handle_mousemove};
1212

13-
use crate::BaseDocument;
13+
use crate::{BaseDocument, events::mouse::handle_wheel};
1414

1515
pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
1616
doc: &mut BaseDocument,
@@ -43,15 +43,20 @@ pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
4343
set_focus = true;
4444
Some(UiEvent::MouseUp(mouse_event))
4545
}
46+
DomEventData::MouseEnter(_) => None,
47+
DomEventData::MouseLeave(_) => None,
48+
DomEventData::MouseOver(_) => None,
49+
DomEventData::MouseOut(_) => None,
4650
DomEventData::KeyDown(data) => Some(UiEvent::KeyDown(data)),
4751
DomEventData::KeyUp(data) => Some(UiEvent::KeyUp(data)),
4852
DomEventData::Ime(data) => Some(UiEvent::Ime(data)),
49-
50-
// Derived events do not map to a UiEvent. We simply ignore them.
51-
// The sub document will generate it's own versions of these events.
5253
DomEventData::KeyPress(_) => None,
5354
DomEventData::Click(_) => None,
55+
DomEventData::ContextMenu(_) => None,
56+
DomEventData::DoubleClick(_) => None,
5457
DomEventData::Input(_) => None,
58+
DomEventData::Wheel(data) => Some(UiEvent::Wheel(data)),
59+
DomEventData::Scroll(_) => None,
5560
};
5661

5762
if let Some(ui_event) = ui_event {
@@ -74,6 +79,8 @@ pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
7479
mouse_event.x,
7580
mouse_event.y,
7681
mouse_event.buttons,
82+
mouse_event,
83+
dispatch_event,
7784
);
7885
if changed {
7986
doc.shell_provider.request_redraw();
@@ -103,5 +110,29 @@ pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
103110
DomEventData::Input(_) => {
104111
// Do nothing (no default action)
105112
}
113+
DomEventData::ContextMenu(_) => {
114+
// TODO: Open context menu
115+
}
116+
DomEventData::DoubleClick(_) => {
117+
// Do nothing (no default action)
118+
}
119+
DomEventData::MouseEnter(_) => {
120+
// Do nothing (no default action)
121+
}
122+
DomEventData::MouseLeave(_) => {
123+
// Do nothing (no default action)
124+
}
125+
DomEventData::MouseOver(_) => {
126+
// Do nothing (no default action)
127+
}
128+
DomEventData::MouseOut(_) => {
129+
// Do nothing (no default action)
130+
}
131+
DomEventData::Scroll(_) => {
132+
// Handled elsewhere
133+
}
134+
DomEventData::Wheel(event) => {
135+
handle_wheel(doc, target_node_id, event.clone(), dispatch_event);
136+
}
106137
}
107138
}

packages/blitz-dom/src/events/mouse.rs

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,38 @@
1+
use std::time::{Duration, Instant};
2+
13
use blitz_traits::{
24
events::{
3-
BlitzInputEvent, BlitzMouseButtonEvent, DomEvent, DomEventData, MouseEventButton,
4-
MouseEventButtons,
5+
BlitzInputEvent, BlitzMouseButtonEvent, BlitzWheelDelta, BlitzWheelEvent, DomEvent,
6+
DomEventData, MouseEventButton, MouseEventButtons,
57
},
68
navigation::NavigationOptions,
79
};
810
use markup5ever::local_name;
911

1012
use crate::{BaseDocument, node::SpecialElementData};
1113

12-
pub(crate) fn handle_mousemove(
14+
pub(crate) fn handle_mousemove<F: FnMut(DomEvent)>(
1315
doc: &mut BaseDocument,
1416
target: usize,
1517
x: f32,
1618
y: f32,
1719
buttons: MouseEventButtons,
20+
event: &BlitzMouseButtonEvent,
21+
mut dispatch_event: F,
1822
) -> bool {
1923
let mut changed = doc.set_hover_to(x, y);
2024

2125
let Some(hit) = doc.hit(x, y) else {
2226
return changed;
2327
};
2428

29+
if changed {
30+
dispatch_event(DomEvent::new(
31+
hit.node_id,
32+
DomEventData::MouseEnter(event.clone()),
33+
));
34+
}
35+
2536
if hit.node_id != target {
2637
return changed;
2738
}
@@ -143,6 +154,14 @@ pub(crate) fn handle_mouseup<F: FnMut(DomEvent)>(
143154
if do_click && event.button == MouseEventButton::Main {
144155
dispatch_event(DomEvent::new(target, DomEventData::Click(event.clone())));
145156
}
157+
158+
// Dispatch a context menu event
159+
if do_click && event.button == MouseEventButton::Secondary {
160+
dispatch_event(DomEvent::new(
161+
target,
162+
DomEventData::ContextMenu(event.clone()),
163+
));
164+
}
146165
}
147166

148167
pub(crate) fn handle_click<F: FnMut(DomEvent)>(
@@ -275,4 +294,46 @@ pub(crate) fn handle_click<F: FnMut(DomEvent)>(
275294

276295
// If nothing is matched then clear focus
277296
doc.clear_focus();
297+
298+
// Assumed double click time to be less than 500ms, although may be system-dependant?
299+
if doc
300+
.last_click_time
301+
.map(|t| t.elapsed() < Duration::from_millis(500))
302+
.unwrap_or(false)
303+
{
304+
doc.last_click_time = Some(Instant::now());
305+
doc.click_count += 1;
306+
307+
if doc.click_count == 2 {
308+
dispatch_event(DomEvent::new(
309+
target,
310+
DomEventData::DoubleClick(event.clone()),
311+
));
312+
}
313+
} else {
314+
doc.last_click_time = Some(Instant::now());
315+
doc.click_count = 1;
316+
}
317+
}
318+
319+
pub(crate) fn handle_wheel<F: FnMut(DomEvent)>(
320+
doc: &mut BaseDocument,
321+
_: usize,
322+
event: BlitzWheelEvent,
323+
dispatch_event: F,
324+
) {
325+
let (scroll_x, scroll_y) = match event.delta {
326+
BlitzWheelDelta::Lines(x, y) => (x * 20.0, y * 20.0),
327+
BlitzWheelDelta::Pixels(x, y) => (x, y),
328+
};
329+
330+
let has_changed = if let Some(hover_node_id) = doc.get_hover_node_id() {
331+
doc.scroll_node_by_has_changed(hover_node_id, scroll_x, scroll_y, dispatch_event)
332+
} else {
333+
doc.scroll_viewport_by_has_changed(scroll_x, scroll_y)
334+
};
335+
336+
if has_changed {
337+
doc.shell_provider.request_redraw();
338+
}
278339
}

0 commit comments

Comments
 (0)