diff --git a/src/dpg_map/interaction.py b/src/dpg_map/interaction.py index bdffb17..0c4fcd3 100644 --- a/src/dpg_map/interaction.py +++ b/src/dpg_map/interaction.py @@ -196,6 +196,31 @@ def handle_mouse_wheel( ) +def update_drag_from_button_state( + state: MapState, + *, + mouse_pos: tuple[float, float], + hit_rect: HitRect, + is_down: bool, +) -> None: + """Poll left-button state and keep drag interaction moving.""" + + with state.lock: + active_drag = state.interaction.active_drag + + if not is_down: + if active_drag: + handle_mouse_release(state) + return + + if active_drag: + handle_mouse_drag(state, mouse_pos) + return + + if hit_rect.contains(mouse_pos[0], mouse_pos[1]): + handle_mouse_down(state, mouse_pos, hit_rect) + + def wheel_delta_from_app_data(app_data: Any) -> float: """Normalize Dear PyGui mouse wheel callback data.""" diff --git a/src/dpg_map/renderer.py b/src/dpg_map/renderer.py index 9dd8c9b..df19cf5 100644 --- a/src/dpg_map/renderer.py +++ b/src/dpg_map/renderer.py @@ -8,7 +8,7 @@ from typing import Any from .commands import CommandKind, MapCommand from .draw_layers import DrawLayerTags, clear_draw_layer, ensure_draw_layers -from .interaction import HitRect, calculate_hit_rect +from .interaction import HitRect, calculate_hit_rect, update_drag_from_button_state from .overlays import MarkerOverlay, Overlay, PolylineOverlay, TrajectoryOverlay from .projection import latlon_to_world from .sizing import SizeMeasurement, apply_size_measurement @@ -53,6 +53,7 @@ class MapRenderer: commands = drain_renderer_commands(self.state) self.last_drained_commands = tuple(commands) self._update_size_from_dpg() + self._poll_mouse_drag() with self.state.lock: dirty = self.state.dirty @@ -132,6 +133,21 @@ class MapRenderer: with self.state.lock: self.last_hit_rect = calculate_hit_rect(self.state, (draw_pos[0], draw_pos[1])) + def _poll_mouse_drag(self) -> None: + if self.last_hit_rect is None: + return + try: + is_down = bool(self._dpg.is_mouse_button_down(self._dpg.mvMouseButton_Left)) + mouse_pos = self._dpg.get_mouse_pos(local=False) + except Exception: + return + update_drag_from_button_state( + self.state, + mouse_pos=(float(mouse_pos[0]), float(mouse_pos[1])), + hit_rect=self.last_hit_rect, + is_down=is_down, + ) + def _measure_child_content(self) -> tuple[int, int]: try: width, height = self._dpg.get_item_rect_size(self.state.child_window_tag) diff --git a/tests/test_interaction.py b/tests/test_interaction.py index 2376ca4..4ec1ee8 100644 --- a/tests/test_interaction.py +++ b/tests/test_interaction.py @@ -11,6 +11,7 @@ from dpg_map.interaction import ( handle_mouse_release, handle_mouse_wheel, pan_state_by_pixels, + update_drag_from_button_state, ) from dpg_map.sizing import SizeMeasurement, apply_size_measurement from dpg_map.state import DirtyFlags, create_map_state, get_map_state @@ -60,6 +61,19 @@ def test_mouse_drag_uses_active_drag_state() -> None: assert state.center[1] < 0.0 +def test_polled_drag_starts_and_moves_while_button_is_down() -> None: + state = create_map_state(tag="polled-drag", center=(0.0, 0.0), zoom=3) + apply_size_measurement(state, SizeMeasurement(width=400, height=300, visible=True)) + rect = calculate_hit_rect(state, (10.0, 20.0)) + + update_drag_from_button_state(state, mouse_pos=(20.0, 30.0), hit_rect=rect, is_down=True) + update_drag_from_button_state(state, mouse_pos=(45.0, 30.0), hit_rect=rect, is_down=True) + update_drag_from_button_state(state, mouse_pos=(45.0, 30.0), hit_rect=rect, is_down=False) + + assert state.interaction.active_drag is False + assert state.center[1] < 0.0 + + def test_wheel_zoom_keeps_cursor_latlon_stable() -> None: state = create_map_state(tag="wheel", center=(47.9029, 1.9093), zoom=8) apply_size_measurement(state, SizeMeasurement(width=800, height=600, visible=True))