use crate::geometry::PropertyValue;
use crate::interaction::{
InteractionButton, InteractionEvent, InteractionEventKind, InteractionModifiers,
InteractionTarget, PointerKind, ScreenPoint,
};
use crate::picking::{PickHit, PickOptions, PickQuery, PickResult};
use crate::query::FeatureStateId;
use crate::MapState;
#[derive(Debug, Clone)]
pub struct InteractionConfig {
pub drag_threshold_px: f64,
pub double_click_window_secs: f64,
pub auto_hover_state: bool,
pub auto_select_state: bool,
pub interactive_layers: Vec<String>,
pub tolerance_meters: f64,
}
impl Default for InteractionConfig {
fn default() -> Self {
Self {
drag_threshold_px: 5.0,
double_click_window_secs: 0.3,
auto_hover_state: true,
auto_select_state: false,
interactive_layers: Vec::new(),
tolerance_meters: 0.0,
}
}
}
#[derive(Debug, Clone)]
struct HitSnapshot {
hit: PickHit,
target: InteractionTarget,
}
#[derive(Debug)]
pub struct InteractionManager {
config: InteractionConfig,
cursor: ScreenPoint,
prev_hit: Option<HitSnapshot>,
hovered: Option<FeatureStateId>,
selected: Option<FeatureStateId>,
pointer_down_pos: Option<ScreenPoint>,
pointer_down_time: Option<f64>,
dragging: bool,
last_click_time: Option<f64>,
current_time: f64,
events: Vec<InteractionEvent>,
}
impl InteractionManager {
pub fn new() -> Self {
Self::with_config(InteractionConfig::default())
}
pub fn with_config(config: InteractionConfig) -> Self {
Self {
config,
cursor: ScreenPoint::default(),
prev_hit: None,
hovered: None,
selected: None,
pointer_down_pos: None,
pointer_down_time: None,
dragging: false,
last_click_time: None,
current_time: 0.0,
events: Vec::new(),
}
}
pub fn config(&self) -> &InteractionConfig {
&self.config
}
pub fn config_mut(&mut self) -> &mut InteractionConfig {
&mut self.config
}
pub fn hovered(&self) -> Option<&FeatureStateId> {
self.hovered.as_ref()
}
pub fn selected(&self) -> Option<&FeatureStateId> {
self.selected.as_ref()
}
pub fn cursor(&self) -> ScreenPoint {
self.cursor
}
pub fn is_dragging(&self) -> bool {
self.dragging
}
pub fn update_pointer_move(
&mut self,
map: &mut MapState,
x: f64,
y: f64,
time: f64,
pointer_kind: PointerKind,
modifiers: InteractionModifiers,
) {
self.current_time = time;
self.cursor = ScreenPoint::new(x, y);
if let Some(down_pos) = self.pointer_down_pos {
let dx = x - down_pos.x;
let dy = y - down_pos.y;
if (dx * dx + dy * dy).sqrt() > self.config.drag_threshold_px {
self.dragging = true;
}
}
let result = self.pick_at(map, x, y);
let current = top_hit_snapshot(&result);
let prev_target = self.prev_hit.as_ref().map(|s| &s.target);
let curr_target = current.as_ref().map(|s| &s.target);
if prev_target != curr_target {
if let Some(prev) = &self.prev_hit {
let mut event =
self.base_event(InteractionEventKind::MouseLeave, pointer_kind, modifiers);
event.target = Some(prev.target.clone());
event.related_target = curr_target.cloned();
self.events.push(event);
}
if self.config.auto_hover_state {
if let Some(prev_id) = self.hovered.take() {
map.set_feature_state_property(
&prev_id.source_id,
&prev_id.feature_id,
"hover",
PropertyValue::Bool(false),
);
}
}
if let Some(curr) = ¤t {
let mut event =
self.base_event(InteractionEventKind::MouseEnter, pointer_kind, modifiers);
event.target = Some(curr.target.clone());
event.hit = Some(curr.hit.clone());
event.related_target = prev_target.cloned();
self.events.push(event);
if self.config.auto_hover_state {
if let Some(id) = feature_state_id_from_target(&curr.target) {
map.set_feature_state_property(
&id.source_id,
&id.feature_id,
"hover",
PropertyValue::Bool(true),
);
self.hovered = Some(id);
}
}
}
}
let mut move_event =
self.base_event(InteractionEventKind::MouseMove, pointer_kind, modifiers);
if let Some(curr) = ¤t {
move_event.target = Some(curr.target.clone());
move_event.hit = Some(curr.hit.clone());
}
self.events.push(move_event);
self.prev_hit = current;
}
#[allow(clippy::too_many_arguments)]
pub fn update_pointer_down(
&mut self,
map: &MapState,
x: f64,
y: f64,
time: f64,
button: InteractionButton,
pointer_kind: PointerKind,
modifiers: InteractionModifiers,
) {
let _ = map; self.current_time = time;
self.pointer_down_pos = Some(ScreenPoint::new(x, y));
self.pointer_down_time = Some(time);
self.dragging = false;
let mut event = self.base_event(InteractionEventKind::MouseDown, pointer_kind, modifiers);
event.button = Some(button);
if let Some(snapshot) = &self.prev_hit {
event.target = Some(snapshot.target.clone());
event.hit = Some(snapshot.hit.clone());
}
self.events.push(event);
}
#[allow(clippy::too_many_arguments)]
pub fn update_pointer_up(
&mut self,
map: &mut MapState,
x: f64,
y: f64,
time: f64,
button: InteractionButton,
pointer_kind: PointerKind,
modifiers: InteractionModifiers,
) {
self.current_time = time;
self.cursor = ScreenPoint::new(x, y);
let mut up_event = self.base_event(InteractionEventKind::MouseUp, pointer_kind, modifiers);
up_event.button = Some(button);
if let Some(snapshot) = &self.prev_hit {
up_event.target = Some(snapshot.target.clone());
up_event.hit = Some(snapshot.hit.clone());
}
self.events.push(up_event);
let is_click = !self.dragging;
if is_click {
let mut click_event =
self.base_event(InteractionEventKind::Click, pointer_kind, modifiers);
click_event.button = Some(button);
if let Some(snapshot) = &self.prev_hit {
click_event.target = Some(snapshot.target.clone());
click_event.hit = Some(snapshot.hit.clone());
}
self.events.push(click_event);
if self.config.auto_select_state {
self.update_selection(map);
}
if let Some(prev_click) = self.last_click_time {
if (time - prev_click) <= self.config.double_click_window_secs {
let mut dbl_event =
self.base_event(InteractionEventKind::DoubleClick, pointer_kind, modifiers);
dbl_event.button = Some(button);
if let Some(snapshot) = &self.prev_hit {
dbl_event.target = Some(snapshot.target.clone());
dbl_event.hit = Some(snapshot.hit.clone());
}
self.events.push(dbl_event);
self.last_click_time = None;
} else {
self.last_click_time = Some(time);
}
} else {
self.last_click_time = Some(time);
}
}
self.pointer_down_pos = None;
self.pointer_down_time = None;
self.dragging = false;
}
pub fn update_pointer_leave(
&mut self,
map: &mut MapState,
pointer_kind: PointerKind,
modifiers: InteractionModifiers,
) {
if let Some(prev) = self.prev_hit.take() {
let mut event =
self.base_event(InteractionEventKind::MouseLeave, pointer_kind, modifiers);
event.target = Some(prev.target);
self.events.push(event);
}
if self.config.auto_hover_state {
if let Some(prev_id) = self.hovered.take() {
map.set_feature_state_property(
&prev_id.source_id,
&prev_id.feature_id,
"hover",
PropertyValue::Bool(false),
);
}
}
self.pointer_down_pos = None;
self.pointer_down_time = None;
self.dragging = false;
}
pub fn drain_events(&mut self) -> Vec<InteractionEvent> {
std::mem::take(&mut self.events)
}
pub fn pending_event_count(&self) -> usize {
self.events.len()
}
fn pick_at(&self, map: &MapState, x: f64, y: f64) -> PickResult {
let mut options = PickOptions::new();
options.tolerance_meters = self.config.tolerance_meters;
options.limit = 1;
map.pick(PickQuery::screen(x, y), options)
}
fn base_event(
&self,
kind: InteractionEventKind,
pointer_kind: PointerKind,
modifiers: InteractionModifiers,
) -> InteractionEvent {
InteractionEvent::new(kind, pointer_kind, self.cursor).with_modifiers(modifiers)
}
fn update_selection(&mut self, map: &mut MapState) {
if let Some(prev_id) = self.selected.take() {
map.set_feature_state_property(
&prev_id.source_id,
&prev_id.feature_id,
"selected",
PropertyValue::Bool(false),
);
}
if let Some(snapshot) = &self.prev_hit {
if let Some(id) = feature_state_id_from_target(&snapshot.target) {
map.set_feature_state_property(
&id.source_id,
&id.feature_id,
"selected",
PropertyValue::Bool(true),
);
self.selected = Some(id);
}
}
}
}
impl Default for InteractionManager {
fn default() -> Self {
Self::new()
}
}
fn top_hit_snapshot(result: &PickResult) -> Option<HitSnapshot> {
result.first().map(|hit| HitSnapshot {
target: InteractionTarget::from_pick_hit(hit),
hit: hit.clone(),
})
}
fn feature_state_id_from_target(target: &InteractionTarget) -> Option<FeatureStateId> {
match (&target.source_id, &target.feature_id) {
(Some(source_id), Some(feature_id)) => {
Some(FeatureStateId::new(source_id.clone(), feature_id.clone()))
}
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::{Feature, FeatureCollection, Geometry, Point};
use crate::layers::{VectorLayer, VectorStyle};
use rustial_math::GeoCoord;
fn map_with_point_at_center() -> MapState {
let mut state = MapState::new();
state.set_viewport(800, 600);
let target = GeoCoord::from_lat_lon(0.0, 0.0);
state.set_camera_target(target);
state.set_camera_distance(500.0);
let fc = FeatureCollection {
features: vec![Feature {
geometry: Geometry::Point(Point { coord: target }),
properties: Default::default(),
}],
};
let vl = VectorLayer::new("points", fc, VectorStyle::default());
state.push_layer(Box::new(vl));
state.update();
state
}
#[test]
fn pointer_move_over_feature_emits_enter_then_move() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
let events = mgr.drain_events();
assert!(
events.len() >= 2,
"expected at least enter + move, got {}",
events.len()
);
assert_eq!(events[0].kind, InteractionEventKind::MouseEnter);
assert_eq!(events[1].kind, InteractionEventKind::MouseMove);
assert!(
events[0].target.is_some(),
"enter event should have a target"
);
}
#[test]
fn pointer_move_away_emits_leave() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_move(
&mut map,
10.0,
10.0,
0.1,
PointerKind::Mouse,
InteractionModifiers::default(),
);
let events = mgr.drain_events();
let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
assert!(
kinds.contains(&InteractionEventKind::MouseLeave),
"expected MouseLeave, got {:?}",
kinds
);
}
#[test]
fn click_within_threshold_emits_click() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_down(
&map,
400.0,
300.0,
1.0,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_up(
&mut map,
401.0,
300.0,
1.1,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
let events = mgr.drain_events();
let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
assert!(kinds.contains(&InteractionEventKind::MouseUp));
assert!(
kinds.contains(&InteractionEventKind::Click),
"expected Click, got {:?}",
kinds
);
}
#[test]
fn drag_beyond_threshold_suppresses_click() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_down(
&map,
400.0,
300.0,
1.0,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_move(
&mut map,
420.0,
320.0,
1.05,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_up(
&mut map,
420.0,
320.0,
1.1,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
let events = mgr.drain_events();
let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
assert!(kinds.contains(&InteractionEventKind::MouseUp));
assert!(
!kinds.contains(&InteractionEventKind::Click),
"Click should be suppressed after drag, got {:?}",
kinds
);
}
#[test]
fn two_clicks_within_window_emit_double_click() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_down(
&map,
400.0,
300.0,
1.0,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.update_pointer_up(
&mut map,
400.0,
300.0,
1.05,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_down(
&map,
400.0,
300.0,
1.2,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.update_pointer_up(
&mut map,
400.0,
300.0,
1.25,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
let events = mgr.drain_events();
let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
assert!(
kinds.contains(&InteractionEventKind::DoubleClick),
"expected DoubleClick, got {:?}",
kinds
);
}
#[test]
fn two_clicks_outside_window_do_not_emit_double_click() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_down(
&map,
400.0,
300.0,
1.0,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.update_pointer_up(
&mut map,
400.0,
300.0,
1.05,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
mgr.update_pointer_down(
&map,
400.0,
300.0,
2.0,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.update_pointer_up(
&mut map,
400.0,
300.0,
2.05,
InteractionButton::Primary,
PointerKind::Mouse,
InteractionModifiers::default(),
);
let events = mgr.drain_events();
let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
assert!(
!kinds.contains(&InteractionEventKind::DoubleClick),
"DoubleClick should NOT fire outside timing window, got {:?}",
kinds
);
}
#[test]
fn drain_events_clears_pending_queue() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
assert!(mgr.pending_event_count() > 0);
let events = mgr.drain_events();
assert!(!events.is_empty());
assert_eq!(mgr.pending_event_count(), 0);
}
#[test]
fn pointer_leave_emits_leave_and_clears_hover() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
assert!(mgr.hovered().is_some() || mgr.prev_hit.is_some());
mgr.update_pointer_leave(
&mut map,
PointerKind::Mouse,
InteractionModifiers::default(),
);
let events = mgr.drain_events();
let kinds: Vec<_> = events.iter().map(|e| e.kind).collect();
assert!(
kinds.contains(&InteractionEventKind::MouseLeave),
"expected MouseLeave on pointer leave, got {:?}",
kinds
);
assert!(mgr.prev_hit.is_none());
}
#[test]
fn auto_hover_state_sets_and_clears_feature_state() {
let mut map = map_with_point_at_center();
let mut mgr = InteractionManager::new();
mgr.update_pointer_move(
&mut map,
400.0,
300.0,
0.0,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
if let Some(id) = mgr.hovered() {
let state = map.feature_state(&id.source_id, &id.feature_id);
let hover_val = state.and_then(|s| s.get("hover")).and_then(|v| v.as_bool());
assert_eq!(hover_val, Some(true));
}
mgr.update_pointer_move(
&mut map,
10.0,
10.0,
0.1,
PointerKind::Mouse,
InteractionModifiers::default(),
);
mgr.drain_events();
assert!(mgr.hovered().is_none());
}
}