Skip to content

Commit 1d6741a

Browse files
committed
Implement double-click, triple-click and shift-click for inputs
Signed-off-by: Nico Burns <[email protected]>
1 parent 9060de0 commit 1d6741a

File tree

2 files changed

+160
-117
lines changed

2 files changed

+160
-117
lines changed

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use crate::{BaseDocument, events::mouse::handle_wheel};
1515
pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
1616
doc: &mut BaseDocument,
1717
event: &mut DomEvent,
18-
dispatch_event: F,
18+
mut dispatch_event: F,
1919
) {
2020
let target_node_id = event.target;
2121

@@ -87,13 +87,13 @@ pub(crate) fn handle_dom_event<F: FnMut(DomEvent)>(
8787
}
8888
}
8989
DomEventData::MouseDown(event) => {
90-
handle_mousedown(doc, target_node_id, event.x, event.y);
90+
handle_mousedown(doc, target_node_id, event.x, event.y, event.mods);
9191
}
9292
DomEventData::MouseUp(event) => {
9393
handle_mouseup(doc, target_node_id, event, dispatch_event);
9494
}
9595
DomEventData::Click(event) => {
96-
handle_click(doc, target_node_id, event, dispatch_event);
96+
handle_click(doc, target_node_id, event, &mut dispatch_event);
9797
}
9898
DomEventData::KeyDown(event) => {
9999
handle_keypress(doc, target_node_id, event.clone(), dispatch_event);

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

Lines changed: 157 additions & 114 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use blitz_traits::{
77
},
88
navigation::NavigationOptions,
99
};
10+
use keyboard_types::Modifiers;
1011
use markup5ever::local_name;
1112

1213
use crate::{BaseDocument, node::SpecialElementData};
@@ -79,7 +80,13 @@ pub(crate) fn handle_mousemove<F: FnMut(DomEvent)>(
7980
changed
8081
}
8182

82-
pub(crate) fn handle_mousedown(doc: &mut BaseDocument, target: usize, x: f32, y: f32) {
83+
pub(crate) fn handle_mousedown(
84+
doc: &mut BaseDocument,
85+
target: usize,
86+
x: f32,
87+
y: f32,
88+
mods: Modifiers,
89+
) {
8390
let Some(hit) = doc.hit(x, y) else {
8491
return;
8592
};
@@ -114,11 +121,36 @@ pub(crate) fn handle_mousedown(doc: &mut BaseDocument, target: usize, x: f32, y:
114121
let x = (hit.x - content_box_offset.x) as f64 * doc.viewport.scale_f64();
115122
let y = (hit.y - content_box_offset.y) as f64 * doc.viewport.scale_f64();
116123

117-
text_input_data
124+
// TODO: Only increment click count if click maps to the same/similar caret position as the previous click
125+
let click_count = if doc
126+
.last_click_time
127+
.map(|t| t.elapsed() < Duration::from_millis(500))
128+
.unwrap_or(false)
129+
{
130+
// Add 1 to doc.click_count because the click count hasn't yet been updated in the mousedown event
131+
doc.click_count + 1
132+
} else {
133+
1
134+
};
135+
136+
let mut font_ctx = doc.font_ctx.lock().unwrap();
137+
let mut driver = text_input_data
118138
.editor
119-
.driver(&mut doc.font_ctx.lock().unwrap(), &mut doc.layout_ctx)
120-
.move_to_point(x as f32, y as f32);
139+
.driver(&mut font_ctx, &mut doc.layout_ctx);
121140

141+
match click_count {
142+
1 => {
143+
if mods.shift() {
144+
driver.shift_click_extension(x as f32, y as f32);
145+
} else {
146+
driver.move_to_point(x as f32, y as f32);
147+
}
148+
}
149+
2 => driver.select_word_at_point(x as f32, y as f32),
150+
_ => driver.select_hard_line_at_point(x as f32, y as f32),
151+
}
152+
153+
drop(font_ctx);
122154
doc.set_focus_to(hit.node_id);
123155
}
124156
}
@@ -164,136 +196,147 @@ pub(crate) fn handle_mouseup<F: FnMut(DomEvent)>(
164196
}
165197
}
166198

167-
pub(crate) fn handle_click<F: FnMut(DomEvent)>(
199+
pub(crate) fn handle_click(
168200
doc: &mut BaseDocument,
169201
target: usize,
170202
event: &BlitzMouseButtonEvent,
171-
mut dispatch_event: F,
203+
dispatch_event: &mut dyn FnMut(DomEvent),
172204
) {
173-
let mut maybe_node_id = Some(target);
174-
while let Some(node_id) = maybe_node_id {
175-
let maybe_element = {
176-
let node = &mut doc.nodes[node_id];
177-
node.data.downcast_element_mut()
178-
};
205+
let double_click_event = event.clone();
179206

180-
let Some(el) = maybe_element else {
181-
maybe_node_id = doc.nodes[node_id].parent;
182-
continue;
183-
};
184-
185-
let disabled = el.attr(local_name!("disabled")).is_some();
186-
if disabled {
187-
return;
188-
}
189-
190-
if let SpecialElementData::TextInput(_) = el.special_data {
191-
return;
192-
}
207+
let mut maybe_node_id = Some(target);
208+
let matched = 'matched: {
209+
while let Some(node_id) = maybe_node_id {
210+
let maybe_element = {
211+
let node = &mut doc.nodes[node_id];
212+
node.data.downcast_element_mut()
213+
};
214+
215+
let Some(el) = maybe_element else {
216+
maybe_node_id = doc.nodes[node_id].parent;
217+
continue;
218+
};
219+
220+
let disabled = el.attr(local_name!("disabled")).is_some();
221+
if disabled {
222+
break 'matched true;
223+
}
193224

194-
match el.name.local {
195-
local_name!("input") if el.attr(local_name!("type")) == Some("checkbox") => {
196-
let is_checked = BaseDocument::toggle_checkbox(el);
197-
let value = is_checked.to_string();
198-
dispatch_event(DomEvent::new(
199-
node_id,
200-
DomEventData::Input(BlitzInputEvent { value }),
201-
));
202-
doc.set_focus_to(node_id);
203-
return;
225+
if let SpecialElementData::TextInput(_) = el.special_data {
226+
break 'matched true;
204227
}
205-
local_name!("input") if el.attr(local_name!("type")) == Some("radio") => {
206-
let radio_set = el.attr(local_name!("name")).unwrap().to_string();
207-
BaseDocument::toggle_radio(doc, radio_set, node_id);
208228

209-
// TODO: make input event conditional on value actually changing
210-
let value = String::from("true");
211-
dispatch_event(DomEvent::new(
212-
node_id,
213-
DomEventData::Input(BlitzInputEvent { value }),
214-
));
229+
match el.name.local {
230+
local_name!("input") if el.attr(local_name!("type")) == Some("checkbox") => {
231+
let is_checked = BaseDocument::toggle_checkbox(el);
232+
let value = is_checked.to_string();
233+
dispatch_event(DomEvent::new(
234+
node_id,
235+
DomEventData::Input(BlitzInputEvent { value }),
236+
));
237+
doc.set_focus_to(node_id);
238+
break 'matched true;
239+
}
240+
local_name!("input") if el.attr(local_name!("type")) == Some("radio") => {
241+
let radio_set = el.attr(local_name!("name")).unwrap().to_string();
242+
BaseDocument::toggle_radio(doc, radio_set, node_id);
243+
244+
// TODO: make input event conditional on value actually changing
245+
let value = String::from("true");
246+
dispatch_event(DomEvent::new(
247+
node_id,
248+
DomEventData::Input(BlitzInputEvent { value }),
249+
));
215250

216-
BaseDocument::set_focus_to(doc, node_id);
251+
BaseDocument::set_focus_to(doc, node_id);
217252

218-
return;
219-
}
220-
// Clicking labels triggers click, and possibly input event, of associated input
221-
local_name!("label") => {
222-
if let Some(target_node_id) = doc.label_bound_input_element(node_id).map(|n| n.id) {
223-
// Apply default click event action for target node
224-
let target_node = doc.get_node_mut(target_node_id).unwrap();
225-
let syn_event = target_node.synthetic_click_event_data(event.mods);
226-
handle_click(doc, target_node_id, &syn_event, dispatch_event);
227-
return;
253+
break 'matched true;
228254
}
229-
}
230-
local_name!("a") => {
231-
if let Some(href) = el.attr(local_name!("href")) {
232-
if let Some(url) = doc.url.resolve_relative(href) {
233-
doc.navigation_provider.navigate_to(NavigationOptions::new(
234-
url,
235-
String::from("text/plain"),
236-
doc.id(),
237-
));
238-
} else {
239-
println!("{href} is not parseable as a url. : {:?}", *doc.url)
255+
// Clicking labels triggers click, and possibly input event, of associated input
256+
local_name!("label") => {
257+
if let Some(target_node_id) =
258+
doc.label_bound_input_element(node_id).map(|n| n.id)
259+
{
260+
// Apply default click event action for target node
261+
let target_node = doc.get_node_mut(target_node_id).unwrap();
262+
let syn_event = target_node.synthetic_click_event_data(event.mods);
263+
handle_click(doc, target_node_id, &syn_event, dispatch_event);
264+
break 'matched true;
240265
}
241-
return;
242-
} else {
243-
println!("Clicked link without href: {:?}", el.attrs());
244266
}
245-
}
246-
local_name!("input")
247-
if el.is_submit_button() || el.attr(local_name!("type")) == Some("submit") =>
248-
{
249-
if let Some(form_owner) = doc.controls_to_form.get(&node_id) {
250-
doc.submit_form(*form_owner, node_id);
267+
local_name!("a") => {
268+
if let Some(href) = el.attr(local_name!("href")) {
269+
if let Some(url) = doc.url.resolve_relative(href) {
270+
doc.navigation_provider.navigate_to(NavigationOptions::new(
271+
url,
272+
String::from("text/plain"),
273+
doc.id(),
274+
));
275+
} else {
276+
println!("{href} is not parseable as a url. : {:?}", *doc.url)
277+
}
278+
break 'matched true;
279+
} else {
280+
println!("Clicked link without href: {:?}", el.attrs());
281+
}
251282
}
252-
}
253-
#[cfg(feature = "file_input")]
254-
local_name!("input") if el.attr(local_name!("type")) == Some("file") => {
255-
use crate::qual_name;
256-
//TODO: Handle accept attribute https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept by passing an appropriate filter
257-
let multiple = el.attr(local_name!("multiple")).is_some();
258-
let files = doc.shell_provider.open_file_dialog(multiple, None);
259-
260-
if let Some(file) = files.first() {
261-
el.attrs
262-
.set(qual_name!("value", html), &file.to_string_lossy());
283+
local_name!("input")
284+
if el.is_submit_button() || el.attr(local_name!("type")) == Some("submit") =>
285+
{
286+
if let Some(form_owner) = doc.controls_to_form.get(&node_id) {
287+
doc.submit_form(*form_owner, node_id);
288+
}
263289
}
264-
let text_content = match files.len() {
265-
0 => "No Files Selected".to_string(),
266-
1 => files
267-
.first()
268-
.unwrap()
269-
.file_name()
270-
.unwrap_or_default()
271-
.to_string_lossy()
272-
.to_string(),
273-
x => format!("{x} Files Selected"),
274-
};
275-
276-
if files.is_empty() {
277-
el.special_data = SpecialElementData::None;
278-
} else {
279-
el.special_data = SpecialElementData::FileInput(files.into())
290+
#[cfg(feature = "file_input")]
291+
local_name!("input") if el.attr(local_name!("type")) == Some("file") => {
292+
use crate::qual_name;
293+
//TODO: Handle accept attribute https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/accept by passing an appropriate filter
294+
let multiple = el.attr(local_name!("multiple")).is_some();
295+
let files = doc.shell_provider.open_file_dialog(multiple, None);
296+
297+
if let Some(file) = files.first() {
298+
el.attrs
299+
.set(qual_name!("value", html), &file.to_string_lossy());
300+
}
301+
let text_content = match files.len() {
302+
0 => "No Files Selected".to_string(),
303+
1 => files
304+
.first()
305+
.unwrap()
306+
.file_name()
307+
.unwrap_or_default()
308+
.to_string_lossy()
309+
.to_string(),
310+
x => format!("{x} Files Selected"),
311+
};
312+
313+
if files.is_empty() {
314+
el.special_data = SpecialElementData::None;
315+
} else {
316+
el.special_data = SpecialElementData::FileInput(files.into())
317+
}
318+
let child_label_id = doc.nodes[node_id].children[1];
319+
let child_text_id = doc.nodes[child_label_id].children[0];
320+
let text_data = doc.nodes[child_text_id]
321+
.text_data_mut()
322+
.expect("Text data not found");
323+
text_data.content = text_content;
280324
}
281-
let child_label_id = doc.nodes[node_id].children[1];
282-
let child_text_id = doc.nodes[child_label_id].children[0];
283-
let text_data = doc.nodes[child_text_id]
284-
.text_data_mut()
285-
.expect("Text data not found");
286-
text_data.content = text_content;
325+
_ => {}
287326
}
288-
_ => {}
327+
328+
// No match. Recurse up to parent.
329+
maybe_node_id = doc.nodes[node_id].parent;
289330
}
290331

291-
// No match. Recurse up to parent.
292-
maybe_node_id = doc.nodes[node_id].parent;
293-
}
332+
// Didn't match anything
333+
false
334+
};
294335

295336
// If nothing is matched then clear focus
296-
doc.clear_focus();
337+
if !matched {
338+
doc.clear_focus();
339+
}
297340

298341
// Assumed double click time to be less than 500ms, although may be system-dependant?
299342
if doc
@@ -307,7 +350,7 @@ pub(crate) fn handle_click<F: FnMut(DomEvent)>(
307350
if doc.click_count == 2 {
308351
dispatch_event(DomEvent::new(
309352
target,
310-
DomEventData::DoubleClick(event.clone()),
353+
DomEventData::DoubleClick(double_click_event),
311354
));
312355
}
313356
} else {

0 commit comments

Comments
 (0)