use std::{
collections::VecDeque,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use blitz_traits::{
events::{
BlitzInputEvent, BlitzPointerEvent, BlitzPointerId, BlitzWheelDelta, BlitzWheelEvent,
DomEvent, DomEventData, MouseEventButton, MouseEventButtons,
},
navigation::NavigationOptions,
};
use keyboard_types::Modifiers;
use markup5ever::local_name;
use crate::{BaseDocument, node::SpecialElementData};
use super::focus::generate_focus_events;
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct FlingState {
pub(crate) target: usize,
pub(crate) last_seen_time: f64,
pub(crate) x_velocity: f64,
pub(crate) y_velocity: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum ScrollAnimationState {
None,
Fling(FlingState),
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PanState {
pub(crate) target: usize,
pub(crate) last_x: f32,
pub(crate) last_y: f32,
pub(crate) samples: VecDeque<PanSample>,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) struct PanSample {
pub(crate) time: u64,
pub(crate) dx: f32,
pub(crate) dy: f32,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum DragMode {
None,
Selecting,
Panning(PanState),
}
impl DragMode {
pub(crate) fn take(&mut self) -> DragMode {
std::mem::replace(self, DragMode::None)
}
}
impl PanState {
fn update(&mut self, time_ms: u64, screen_x: f32, screen_y: f32) -> (f64, f64) {
let dx = (screen_x - self.last_x) as f64;
let dy = (screen_y - self.last_y) as f64;
self.last_x = screen_x;
self.last_y = screen_y;
self.samples.push_back(PanSample {
time: time_ms,
dx: dx as f32,
dy: dy as f32,
});
if self.samples.len() > 50 && time_ms - self.samples.front().unwrap().time > 100 {
let idx = self
.samples
.partition_point(|sample| time_ms - sample.time > 100);
for _ in 0..idx {
self.samples.pop_front();
}
}
(dx, dy)
}
fn generate_fling(&self, time_ms: u64) -> Option<FlingState> {
if let Some(last_sample) = self.samples.back()
&& time_ms - last_sample.time < 100
{
let idx = self
.samples
.partition_point(|sample| time_ms - sample.time > 100);
let pan_start_time = self.samples[idx].time;
let pan_time = (time_ms - pan_start_time) as f32;
if pan_time > 0.0 {
let (pan_x, pan_y) = self
.samples
.iter()
.skip(idx)
.fold((0.0, 0.0), |(dx, dy), sample| {
(dx + sample.dx, dy + sample.dy)
});
let x_velocity = if pan_x.abs() > pan_y.abs() {
pan_x / pan_time
} else {
0.0
};
let y_velocity = if pan_y.abs() > pan_x.abs() {
pan_y / pan_time
} else {
0.0
};
return Some(FlingState {
target: self.target,
last_seen_time: time_ms as f64,
x_velocity: x_velocity as f64 * 2.0,
y_velocity: y_velocity as f64 * 2.0,
});
}
}
None
}
}
pub(crate) fn handle_pointermove<F: FnMut(DomEvent)>(
doc: &mut BaseDocument,
target: usize,
event: &BlitzPointerEvent,
mut dispatch_event: F,
) -> bool {
let x = event.page_x();
let y = event.page_y();
let buttons = event.buttons;
let mut changed = doc.set_hover_to(x, y);
if buttons != MouseEventButtons::None && doc.drag_mode == DragMode::None {
let dx = x - doc.mousedown_position.x;
let dy = y - doc.mousedown_position.y;
if dx.abs() > 2.0 || dy.abs() > 2.0 {
match event.id {
BlitzPointerId::Mouse | BlitzPointerId::Pen => {
doc.drag_mode = DragMode::Selecting;
}
BlitzPointerId::Finger(_) => {
doc.drag_mode = DragMode::Panning(PanState {
target,
last_x: event.screen_x(),
last_y: event.screen_y(),
samples: VecDeque::with_capacity(200),
});
}
}
}
}
if let DragMode::Panning(state) = &mut doc.drag_mode {
let time_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
let target = state.target;
let (dx, dy) = state.update(time_ms, event.screen_x(), event.screen_y());
let has_changed = doc.scroll_by(Some(target), dx, dy, &mut dispatch_event);
return has_changed;
}
let Some(hit) = doc.hit(x, y) else {
return changed;
};
if changed {
dispatch_event(DomEvent::new(
hit.node_id,
DomEventData::MouseEnter(event.clone()),
));
}
if hit.node_id != target {
return changed;
}
let node = &mut doc.nodes[target];
let Some(el) = node.data.downcast_element_mut() else {
if buttons != MouseEventButtons::None && doc.extend_text_selection_to_point(x, y) {
changed = true;
}
return changed;
};
let disabled = el.attr(local_name!("disabled")).is_some();
if disabled {
return changed;
}
if let SpecialElementData::TextInput(ref mut text_input_data) = el.special_data {
if buttons == MouseEventButtons::None {
return changed;
}
let mut content_box_offset = taffy::Point {
x: node.final_layout.padding.left + node.final_layout.border.left,
y: node.final_layout.padding.top + node.final_layout.border.top,
};
if !text_input_data.is_multiline {
let layout = text_input_data.editor.try_layout().unwrap();
let content_box_height = node.final_layout.content_box_height();
let input_height = layout.height() / layout.scale();
let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
content_box_offset.y += y_offset;
}
let x = (hit.x - content_box_offset.x) as f64 * doc.viewport.scale_f64();
let y = (hit.y - content_box_offset.y) as f64 * doc.viewport.scale_f64();
text_input_data
.editor
.driver(&mut doc.font_ctx.lock().unwrap(), &mut doc.layout_ctx)
.extend_selection_to_point(x as f32, y as f32);
changed = true;
} else if event.is_mouse()
&& buttons != MouseEventButtons::None
&& doc.extend_text_selection_to_point(x, y)
{
changed = true;
}
changed
}
pub(crate) fn handle_pointerdown(
doc: &mut BaseDocument,
_target: usize,
x: f32,
y: f32,
mods: Modifiers,
dispatch_event: &mut dyn FnMut(DomEvent),
) {
doc.click_count = if doc
.last_mousedown_time
.map(|t| t.elapsed() < Duration::from_millis(500))
.unwrap_or(false)
&& (doc.mousedown_position.x - x).abs() <= 2.0
&& (doc.mousedown_position.y - y).abs() <= 2.0
{
doc.click_count + 1
} else {
1
};
doc.last_mousedown_time = Some(Instant::now());
doc.mousedown_position = taffy::Point { x, y };
doc.drag_mode = DragMode::None;
doc.scroll_animation = ScrollAnimationState::None;
let Some(hit) = doc.hit(x, y) else {
doc.clear_text_selection();
return;
};
let actual_target = hit.node_id;
enum ClickTarget {
TextInput {
content_box_offset: taffy::Point<f32>,
},
Disabled,
SelectableText,
}
let click_target = {
let node = &doc.nodes[actual_target];
match node.data.downcast_element() {
Some(el) if el.has_attr(local_name!("disabled")) => ClickTarget::Disabled,
Some(el) => {
if let SpecialElementData::TextInput(ref text_input_data) = el.special_data {
let mut content_box_offset = taffy::Point {
x: node.final_layout.padding.left + node.final_layout.border.left,
y: node.final_layout.padding.top + node.final_layout.border.top,
};
if !text_input_data.is_multiline {
let layout = text_input_data.editor.try_layout().unwrap();
let content_box_height = node.final_layout.content_box_height();
let input_height = layout.height() / layout.scale();
let y_offset = ((content_box_height - input_height) / 2.0).max(0.0);
content_box_offset.y += y_offset;
}
ClickTarget::TextInput { content_box_offset }
} else {
ClickTarget::SelectableText
}
}
None => ClickTarget::SelectableText,
}
};
match click_target {
ClickTarget::Disabled => (),
ClickTarget::SelectableText => {
if let Some((inline_root_id, byte_offset)) = doc.find_text_position(x, y) {
doc.set_text_selection(inline_root_id, byte_offset, inline_root_id, byte_offset);
doc.shell_provider.request_redraw();
} else {
doc.clear_text_selection();
}
}
ClickTarget::TextInput { content_box_offset } => {
doc.clear_text_selection();
let tx = (hit.x - content_box_offset.x) as f64 * doc.viewport.scale_f64();
let ty = (hit.y - content_box_offset.y) as f64 * doc.viewport.scale_f64();
let click_count = doc.click_count;
let node = &mut doc.nodes[actual_target];
let el = node.data.downcast_element_mut().unwrap();
if let SpecialElementData::TextInput(ref mut text_input_data) = el.special_data {
let mut font_ctx = doc.font_ctx.lock().unwrap();
let mut driver = text_input_data
.editor
.driver(&mut font_ctx, &mut doc.layout_ctx);
match click_count {
1 => {
if mods.shift() {
driver.shift_click_extension(tx as f32, ty as f32);
} else {
driver.move_to_point(tx as f32, ty as f32);
}
}
2 => driver.select_word_at_point(tx as f32, ty as f32),
_ => driver.select_hard_line_at_point(tx as f32, ty as f32),
}
drop(font_ctx);
}
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(hit.node_id);
},
dispatch_event,
);
}
}
}
pub(crate) fn handle_pointerup<F: FnMut(DomEvent)>(
doc: &mut BaseDocument,
target: usize,
event: &BlitzPointerEvent,
mut dispatch_event: F,
) {
if doc.devtools().highlight_hover {
let mut node = doc.get_node(target).unwrap();
if event.button == MouseEventButton::Secondary {
if let Some(parent_id) = node.layout_parent.get() {
node = doc.get_node(parent_id).unwrap();
}
}
doc.debug_log_node(node.id);
doc.devtools_mut().highlight_hover = false;
return;
}
let drag_mode = doc.drag_mode.take();
let do_click = drag_mode == DragMode::None;
let time_ms = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
if let DragMode::Panning(state) = &drag_mode {
if let Some(fling) = state.generate_fling(time_ms) {
doc.scroll_animation = ScrollAnimationState::Fling(fling);
doc.shell_provider.request_redraw();
}
}
if do_click && event.button == MouseEventButton::Main {
dispatch_event(DomEvent::new(target, DomEventData::Click(event.clone())));
}
if do_click && event.button == MouseEventButton::Secondary {
dispatch_event(DomEvent::new(
target,
DomEventData::ContextMenu(event.clone()),
));
}
}
pub(crate) fn handle_click(
doc: &mut BaseDocument,
target: usize,
event: &BlitzPointerEvent,
dispatch_event: &mut dyn FnMut(DomEvent),
) {
let double_click_event = event.clone();
let mut maybe_node_id = Some(target);
let matched = 'matched: {
while let Some(node_id) = maybe_node_id {
let maybe_element = {
let node = &mut doc.nodes[node_id];
node.data.downcast_element_mut()
};
let Some(el) = maybe_element else {
maybe_node_id = doc.nodes[node_id].parent;
continue;
};
let disabled = el.attr(local_name!("disabled")).is_some();
if disabled {
break 'matched true;
}
if let SpecialElementData::TextInput(_) = el.special_data {
break 'matched true;
}
match el.name.local {
local_name!("input") if el.attr(local_name!("type")) == Some("checkbox") => {
let is_checked = BaseDocument::toggle_checkbox(el);
let value = is_checked.to_string();
dispatch_event(DomEvent::new(
node_id,
DomEventData::Input(BlitzInputEvent { value }),
));
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(node_id);
},
dispatch_event,
);
break 'matched true;
}
local_name!("input") if el.attr(local_name!("type")) == Some("radio") => {
let radio_set = el.attr(local_name!("name")).unwrap().to_string();
BaseDocument::toggle_radio(doc, radio_set, node_id);
let value = String::from("true");
dispatch_event(DomEvent::new(
node_id,
DomEventData::Input(BlitzInputEvent { value }),
));
generate_focus_events(
doc,
&mut |doc| {
doc.set_focus_to(node_id);
},
dispatch_event,
);
break 'matched true;
}
local_name!("label") => {
if let Some(target_node_id) =
doc.label_bound_input_element(node_id).map(|n| n.id)
{
let target_node = doc.get_node_mut(target_node_id).unwrap();
let syn_event = target_node.synthetic_click_event_data(event.mods);
handle_click(doc, target_node_id, &syn_event, dispatch_event);
break 'matched true;
}
}
local_name!("a") => {
if let Some(href) = el.attr(local_name!("href")) {
if let Some(url) = doc.url.resolve_relative(href) {
doc.navigation_provider.navigate_to(NavigationOptions::new(
url,
String::from("text/plain"),
doc.id(),
));
} else {
#[cfg(feature = "tracing")]
tracing::warn!("{href} is not parseable as a url. : {:?}", *doc.url);
}
break 'matched true;
} else {
#[cfg(feature = "tracing")]
tracing::info!("Clicked link without href: {:?}", el.attrs());
}
}
local_name!("input")
if el.is_submit_button() || el.attr(local_name!("type")) == Some("submit") =>
{
if let Some(form_owner) = doc.controls_to_form.get(&node_id) {
doc.submit_form(*form_owner, node_id);
}
}
#[cfg(feature = "file_input")]
local_name!("input") if el.attr(local_name!("type")) == Some("file") => {
use crate::qual_name;
let multiple = el.attr(local_name!("multiple")).is_some();
let files = doc.shell_provider.open_file_dialog(multiple, None);
if let Some(file) = files.first() {
el.attrs
.set(qual_name!("value", html), &file.to_string_lossy());
}
let text_content = match files.len() {
0 => "No Files Selected".to_string(),
1 => files
.first()
.unwrap()
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
x => format!("{x} Files Selected"),
};
if files.is_empty() {
el.special_data = SpecialElementData::None;
} else {
el.special_data = SpecialElementData::FileInput(files.into())
}
let child_label_id = doc.nodes[node_id].children[1];
let child_text_id = doc.nodes[child_label_id].children[0];
let text_data = doc.nodes[child_text_id]
.text_data_mut()
.expect("Text data not found");
text_data.content = text_content;
}
_ => {}
}
maybe_node_id = doc.nodes[node_id].parent;
}
false
};
if !matched {
generate_focus_events(doc, &mut |doc| doc.clear_focus(), dispatch_event);
}
if doc.click_count == 2 {
dispatch_event(DomEvent::new(
target,
DomEventData::DoubleClick(double_click_event),
));
}
}
pub(crate) fn handle_wheel<F: FnMut(DomEvent)>(
doc: &mut BaseDocument,
_: usize,
event: BlitzWheelEvent,
mut dispatch_event: F,
) {
let (scroll_x, scroll_y) = match event.delta {
BlitzWheelDelta::Lines(x, y) => (x * 20.0, y * 20.0),
BlitzWheelDelta::Pixels(x, y) => (x, y),
};
let has_changed = doc.scroll_by(
doc.get_hover_node_id(),
scroll_x,
scroll_y,
&mut dispatch_event,
);
if has_changed {
doc.shell_provider.request_redraw();
}
}