step 3: add widget shell sizing and frame pump
This commit is contained in:
14
AGENTS.md
14
AGENTS.md
@@ -2,16 +2,17 @@
|
||||
|
||||
## Current status
|
||||
|
||||
Step 2 complete.
|
||||
Step 3 complete.
|
||||
|
||||
## Completed steps
|
||||
|
||||
Step 1 - Public API contract and pure core.
|
||||
Step 2 - Thread-safe state, commands, overlays, and cache model.
|
||||
Step 3 - Widget shell, sizing system, and GUI-thread frame pump.
|
||||
|
||||
## Current step
|
||||
|
||||
Step 3 - Widget shell, sizing system, and GUI-thread frame pump.
|
||||
Step 4 - Tile manager, persistent cache, and asynchronous loading.
|
||||
|
||||
## Design decisions
|
||||
|
||||
@@ -40,6 +41,13 @@ None yet.
|
||||
- Implemented public runtime overlay/view/layer/provider/cache/debug wrappers against logical state without Dear PyGui calls.
|
||||
- Implemented memory cache metadata, disk cache path generation, metadata read/write, disk size scanning, and prune planning.
|
||||
- Added Step 2 tests for command coalescing, overlay/view isolation, copied trajectory inputs, coordinate length validation, layer state, disk path generation, and prune ordering.
|
||||
- Implemented `map_widget(...)` as a Dear PyGui child-window plus drawlist shell.
|
||||
- Implemented GUI-thread renderer frame pump that schedules frame callbacks, drains command queues, measures size, resizes the drawlist, and draws a placeholder background/attribution.
|
||||
- Implemented sizing helpers for measured size, last non-zero size preservation, visibility transitions, effective draw size, and resize dirty flags.
|
||||
- Implemented map interaction hit-rectangle calculation.
|
||||
- Added Step 3 examples for basic map, window sizing, child-window sizing, table sizing, and hidden-tab sizing.
|
||||
- Added Step 3 tests for sizing transitions, zero-size preservation, resize dirty flags, command drain ordering, and hit rectangles.
|
||||
- Ran a Dear PyGui context smoke check for `map_widget` child-window/drawlist creation.
|
||||
- Ran `uv run pytest`.
|
||||
- Ran `uv run ruff check .`.
|
||||
- Ran `uv run ruff format .`.
|
||||
@@ -48,4 +56,4 @@ None yet.
|
||||
|
||||
## Next action
|
||||
|
||||
Implement Step 3.
|
||||
Implement Step 4.
|
||||
|
||||
22
README.md
22
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
`dpg-map` is a Dear PyGui map widget package under rebuild.
|
||||
|
||||
The Step 2 logical runtime model is in place:
|
||||
The Step 3 widget shell is in place:
|
||||
|
||||
```python
|
||||
import dpg_map as dpgm
|
||||
@@ -29,7 +29,21 @@ Implemented so far:
|
||||
- command queue with coalescing for overlay and view updates
|
||||
- logical marker, polyline, trajectory, and layer models
|
||||
- persistent disk cache path, metadata, scanning, and prune planning
|
||||
- Dear PyGui `child_window` + measured-size `drawlist` widget shell
|
||||
- GUI-thread frame pump that drains commands and redraws a placeholder background
|
||||
- sizing helpers that preserve the last non-zero size across hidden layouts
|
||||
- interaction hit-rectangle calculation for the measured map area
|
||||
|
||||
Dear PyGui rendering is not implemented yet. The current `map_widget` is a logical
|
||||
context manager used to register state; Step 3 will add the real child window,
|
||||
drawlist, sizing, and GUI-thread frame pump.
|
||||
Real tile loading and overlay drawing are not implemented yet. Step 4 will add
|
||||
the asynchronous tile manager, persistent tile loading, and GUI-thread texture
|
||||
creation.
|
||||
|
||||
Examples:
|
||||
|
||||
```bash
|
||||
uv run python examples/basic_map.py
|
||||
uv run python examples/sizing_window.py
|
||||
uv run python examples/sizing_child.py
|
||||
uv run python examples/sizing_table.py
|
||||
uv run python examples/hidden_tab.py
|
||||
```
|
||||
|
||||
27
examples/basic_map.py
Normal file
27
examples/basic_map.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import Any
|
||||
|
||||
import dearpygui.dearpygui as _dpg
|
||||
|
||||
import dpg_map as dpgm
|
||||
|
||||
dpg: Any = _dpg
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
dpg.create_viewport(title="dpg-map basic", width=900, height=600)
|
||||
|
||||
with (
|
||||
dpg.window(label="Map", width=-1, height=-1),
|
||||
dpgm.map_widget(tag="map", center=(47.9029, 1.9093), zoom=15, width=-1, height=-1),
|
||||
):
|
||||
dpgm.add_marker("vehicle", lat=47.9029, lon=1.9093)
|
||||
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
27
examples/hidden_tab.py
Normal file
27
examples/hidden_tab.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import Any
|
||||
|
||||
import dearpygui.dearpygui as _dpg
|
||||
|
||||
import dpg_map as dpgm
|
||||
|
||||
dpg: Any = _dpg
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
dpg.create_viewport(title="dpg-map hidden tab", width=900, height=600)
|
||||
|
||||
with dpg.window(label="Hidden Tab Sizing", width=-1, height=-1), dpg.tab_bar():
|
||||
with dpg.tab(label="First"):
|
||||
dpg.add_text("Switch to the map tab.")
|
||||
with dpg.tab(label="Map"), dpgm.map_widget(tag="map-hidden-tab", width=-1, height=500):
|
||||
dpgm.add_marker("tab-marker", lat=35.0, lon=139.0)
|
||||
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
examples/sizing_child.py
Normal file
28
examples/sizing_child.py
Normal file
@@ -0,0 +1,28 @@
|
||||
from typing import Any
|
||||
|
||||
import dearpygui.dearpygui as _dpg
|
||||
|
||||
import dpg_map as dpgm
|
||||
|
||||
dpg: Any = _dpg
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
dpg.create_viewport(title="dpg-map child sizing", width=900, height=600)
|
||||
|
||||
with (
|
||||
dpg.window(label="Nested Child", width=-1, height=-1),
|
||||
dpg.child_window(width=-1, height=420),
|
||||
dpgm.map_widget(tag="map-child", width=-1, height=-1),
|
||||
):
|
||||
dpgm.add_marker("inside-child", lat=47.0, lon=2.0)
|
||||
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
33
examples/sizing_table.py
Normal file
33
examples/sizing_table.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from typing import Any
|
||||
|
||||
import dearpygui.dearpygui as _dpg
|
||||
|
||||
import dpg_map as dpgm
|
||||
|
||||
dpg: Any = _dpg
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
dpg.create_viewport(title="dpg-map table sizing", width=1000, height=600)
|
||||
|
||||
with (
|
||||
dpg.window(label="Table Layout", width=-1, height=-1),
|
||||
dpg.table(header_row=True, resizable=True, policy=dpg.mvTable_SizingStretchProp),
|
||||
):
|
||||
dpg.add_table_column(label="Map")
|
||||
dpg.add_table_column(label="Controls")
|
||||
with dpg.table_row():
|
||||
with dpg.table_cell(), dpgm.map_widget(tag="map-table", width=-1, height=500):
|
||||
dpgm.add_marker("table-marker", lat=51.5, lon=-0.1)
|
||||
with dpg.table_cell():
|
||||
dpg.add_text("Resize the window and table columns.")
|
||||
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
27
examples/sizing_window.py
Normal file
27
examples/sizing_window.py
Normal file
@@ -0,0 +1,27 @@
|
||||
from typing import Any
|
||||
|
||||
import dearpygui.dearpygui as _dpg
|
||||
|
||||
import dpg_map as dpgm
|
||||
|
||||
dpg: Any = _dpg
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
dpg.create_viewport(title="dpg-map window sizing", width=900, height=600)
|
||||
|
||||
with (
|
||||
dpg.window(label="Fill Window", width=-1, height=-1),
|
||||
dpgm.map_widget(tag="map-window", width=-1, height=-1),
|
||||
):
|
||||
dpgm.add_marker("center", lat=0.0, lon=0.0)
|
||||
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +1,36 @@
|
||||
"""Map interaction state and handlers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .sizing import effective_draw_size
|
||||
from .state import MapState
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HitRect:
|
||||
"""Screen-space rectangle used for map interaction tests."""
|
||||
|
||||
x: float
|
||||
y: float
|
||||
width: float
|
||||
height: float
|
||||
|
||||
@property
|
||||
def right(self) -> float:
|
||||
return self.x + self.width
|
||||
|
||||
@property
|
||||
def bottom(self) -> float:
|
||||
return self.y + self.height
|
||||
|
||||
def contains(self, x: float, y: float) -> bool:
|
||||
return self.x <= x <= self.right and self.y <= y <= self.bottom
|
||||
|
||||
|
||||
def calculate_hit_rect(state: MapState, drawlist_pos: tuple[float, float]) -> HitRect:
|
||||
"""Return the map interaction rectangle for a drawlist position."""
|
||||
|
||||
width, height = effective_draw_size(state)
|
||||
return HitRect(float(drawlist_pos[0]), float(drawlist_pos[1]), float(width), float(height))
|
||||
|
||||
@@ -1 +1,157 @@
|
||||
"""GUI-thread renderer implementation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from .commands import CommandKind, MapCommand
|
||||
from .interaction import HitRect, calculate_hit_rect
|
||||
from .sizing import SizeMeasurement, apply_size_measurement
|
||||
from .state import DirtyFlags, MapState
|
||||
|
||||
|
||||
class MapRenderer:
|
||||
"""GUI-thread renderer for the Step 3 widget shell."""
|
||||
|
||||
def __init__(self, state: MapState, dpg: Any) -> None:
|
||||
self.state = state
|
||||
self._dpg = dpg
|
||||
self._background_tag = f"{state.tag}##background"
|
||||
self._title_tag = f"{state.tag}##placeholder-title"
|
||||
self._attribution_tag = f"{state.tag}##attribution"
|
||||
self.last_drained_commands: tuple[MapCommand, ...] = ()
|
||||
self.last_hit_rect: HitRect | None = None
|
||||
|
||||
def schedule_next_frame(self) -> None:
|
||||
"""Schedule this renderer to run on the next Dear PyGui frame."""
|
||||
|
||||
with self.state.lock:
|
||||
if self.state.frame_scheduled:
|
||||
return
|
||||
self.state.frame_scheduled = True
|
||||
frame = self._dpg.get_frame_count() + 1
|
||||
self._dpg.set_frame_callback(frame, self._frame_callback)
|
||||
|
||||
def _frame_callback(self, sender: Any | None = None, app_data: Any | None = None) -> None:
|
||||
_ = (sender, app_data)
|
||||
with self.state.lock:
|
||||
self.state.frame_scheduled = False
|
||||
if not self._dpg.does_item_exist(self.state.drawlist_tag):
|
||||
return
|
||||
self.render_frame()
|
||||
self.schedule_next_frame()
|
||||
|
||||
def render_frame(self) -> None:
|
||||
"""Drain pending commands, refresh size, and draw the placeholder shell."""
|
||||
|
||||
commands = drain_renderer_commands(self.state)
|
||||
self.last_drained_commands = tuple(commands)
|
||||
self._update_size_from_dpg()
|
||||
|
||||
with self.state.lock:
|
||||
dirty = self.state.dirty
|
||||
should_draw = bool(dirty & (DirtyFlags.FULL | DirtyFlags.SIZE))
|
||||
visible = self.state.is_visible
|
||||
width = self.state.measured_width or self.state.last_nonzero_width
|
||||
height = self.state.measured_height or self.state.last_nonzero_height
|
||||
provider_attribution = self.state.provider.attribution
|
||||
self.state.dirty = DirtyFlags.NONE
|
||||
|
||||
if should_draw and visible:
|
||||
self._draw_placeholder(width, height, provider_attribution)
|
||||
|
||||
def _update_size_from_dpg(self) -> None:
|
||||
width, height = self._measure_child_content()
|
||||
visible = bool(self._dpg.is_item_shown(self.state.child_window_tag))
|
||||
with self.state.lock:
|
||||
update = apply_size_measurement(
|
||||
self.state,
|
||||
SizeMeasurement(width=width, height=height, visible=visible),
|
||||
)
|
||||
draw_width = update.effective_width
|
||||
draw_height = update.effective_height
|
||||
self._dpg.configure_item(self.state.drawlist_tag, width=draw_width, height=draw_height)
|
||||
draw_pos = tuple(float(value) for value in self._dpg.get_item_pos(self.state.drawlist_tag))
|
||||
with self.state.lock:
|
||||
self.last_hit_rect = calculate_hit_rect(self.state, (draw_pos[0], draw_pos[1]))
|
||||
|
||||
def _measure_child_content(self) -> tuple[int, int]:
|
||||
try:
|
||||
width, height = self._dpg.get_item_rect_size(self.state.child_window_tag)
|
||||
except Exception:
|
||||
return (0, 0)
|
||||
return (max(0, int(width)), max(0, int(height)))
|
||||
|
||||
def _draw_placeholder(self, width: int, height: int, attribution: str) -> None:
|
||||
width = max(1, int(width))
|
||||
height = max(1, int(height))
|
||||
self._dpg.delete_item(self.state.drawlist_tag, children_only=True)
|
||||
self._dpg.draw_rectangle(
|
||||
(0, 0),
|
||||
(width, height),
|
||||
parent=self.state.drawlist_tag,
|
||||
tag=self._background_tag,
|
||||
color=(54, 68, 78, 255),
|
||||
fill=(29, 38, 45, 255),
|
||||
)
|
||||
self._dpg.draw_text(
|
||||
(12, 12),
|
||||
"dpg-map",
|
||||
parent=self.state.drawlist_tag,
|
||||
tag=self._title_tag,
|
||||
color=(232, 238, 242, 255),
|
||||
size=18,
|
||||
)
|
||||
label = attribution or "Map tiles load in Step 4"
|
||||
text_y = max(28, height - 24)
|
||||
self._dpg.draw_text(
|
||||
(12, text_y),
|
||||
label,
|
||||
parent=self.state.drawlist_tag,
|
||||
tag=self._attribution_tag,
|
||||
color=(172, 184, 192, 255),
|
||||
size=12,
|
||||
)
|
||||
|
||||
|
||||
def drain_renderer_commands(state: MapState) -> list[MapCommand]:
|
||||
"""Drain and apply GUI-thread command side effects that exist in Step 3."""
|
||||
|
||||
commands = state.command_queue.drain()
|
||||
if not commands:
|
||||
return []
|
||||
|
||||
with state.lock:
|
||||
for command in commands:
|
||||
if command.kind is CommandKind.SET_VIEW:
|
||||
state.dirty |= DirtyFlags.VIEW | DirtyFlags.TILES | DirtyFlags.OVERLAYS
|
||||
elif command.kind is CommandKind.SET_PROVIDER:
|
||||
state.dirty |= DirtyFlags.PROVIDER | DirtyFlags.TILES
|
||||
elif command.kind in {
|
||||
CommandKind.ADD_OVERLAY,
|
||||
CommandKind.UPDATE_OVERLAY,
|
||||
CommandKind.DELETE_OVERLAY,
|
||||
CommandKind.SET_LAYER_VISIBILITY,
|
||||
CommandKind.ADD_LAYER,
|
||||
CommandKind.CLEAR_LAYER,
|
||||
}:
|
||||
state.dirty |= DirtyFlags.OVERLAYS
|
||||
elif command.kind is CommandKind.CLEAR_MAP:
|
||||
state.dirty |= DirtyFlags.TILES | DirtyFlags.OVERLAYS
|
||||
elif command.kind in {
|
||||
CommandKind.CLEAR_MEMORY_CACHE,
|
||||
CommandKind.CLEAR_DISK_CACHE,
|
||||
}:
|
||||
state.dirty |= DirtyFlags.TILES
|
||||
return commands
|
||||
|
||||
|
||||
def make_frame_pump(state: MapState, dpg: Any) -> Callable[[], None]:
|
||||
"""Create and attach a renderer frame pump for a map state."""
|
||||
|
||||
renderer = MapRenderer(state, dpg)
|
||||
with state.lock:
|
||||
state.renderer = renderer
|
||||
renderer.schedule_next_frame()
|
||||
return renderer.render_frame
|
||||
|
||||
@@ -1 +1,87 @@
|
||||
"""Map sizing measurement helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .state import DirtyFlags, MapState, mark_dirty
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SizeMeasurement:
|
||||
"""Concrete size and visibility reported by the GUI thread."""
|
||||
|
||||
width: int
|
||||
height: int
|
||||
visible: bool
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class SizeUpdate:
|
||||
"""Result of applying one GUI size measurement."""
|
||||
|
||||
changed: bool
|
||||
became_visible: bool
|
||||
became_hidden: bool
|
||||
effective_width: int
|
||||
effective_height: int
|
||||
|
||||
|
||||
def normalize_dimension(value: int | float | None) -> int:
|
||||
"""Convert Dear PyGui size values to stable integer pixels."""
|
||||
|
||||
if value is None:
|
||||
return 0
|
||||
return max(0, int(value))
|
||||
|
||||
|
||||
def effective_draw_size(state: MapState) -> tuple[int, int]:
|
||||
"""Return the size the drawlist should use for the current frame."""
|
||||
|
||||
width = state.measured_width or state.last_nonzero_width or 1
|
||||
height = state.measured_height or state.last_nonzero_height or 1
|
||||
return (max(1, width), max(1, height))
|
||||
|
||||
|
||||
def apply_size_measurement(
|
||||
state: MapState,
|
||||
measurement: SizeMeasurement,
|
||||
*,
|
||||
mark: bool = True,
|
||||
) -> SizeUpdate:
|
||||
"""Apply a GUI-thread size measurement to logical state.
|
||||
|
||||
A zero measurement is preserved as the current measured size, but it does not
|
||||
erase the last non-zero size. This keeps hidden maps from permanently
|
||||
collapsing when Dear PyGui reports a temporary zero content region.
|
||||
"""
|
||||
|
||||
width = normalize_dimension(measurement.width)
|
||||
height = normalize_dimension(measurement.height)
|
||||
visible = bool(measurement.visible and width > 0 and height > 0)
|
||||
|
||||
previous_width = state.measured_width
|
||||
previous_height = state.measured_height
|
||||
previous_visible = state.is_visible
|
||||
|
||||
changed = previous_width != width or previous_height != height or previous_visible != visible
|
||||
|
||||
state.measured_width = width
|
||||
state.measured_height = height
|
||||
state.is_visible = visible
|
||||
if width > 0:
|
||||
state.last_nonzero_width = width
|
||||
if height > 0:
|
||||
state.last_nonzero_height = height
|
||||
|
||||
if changed and mark:
|
||||
mark_dirty(state, DirtyFlags.SIZE | DirtyFlags.TILES | DirtyFlags.OVERLAYS)
|
||||
|
||||
effective_width, effective_height = effective_draw_size(state)
|
||||
return SizeUpdate(
|
||||
changed=changed,
|
||||
became_visible=visible and not previous_visible,
|
||||
became_hidden=previous_visible and not visible,
|
||||
effective_width=effective_width,
|
||||
effective_height=effective_height,
|
||||
)
|
||||
|
||||
@@ -5,8 +5,10 @@ from __future__ import annotations
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .providers import TileProvider
|
||||
from .renderer import MapRenderer
|
||||
from .state import create_map_state, current_map_context
|
||||
from .types import LatLon, Tag
|
||||
|
||||
@@ -24,11 +26,7 @@ def map_widget(
|
||||
autosize_y: bool = False,
|
||||
**kwargs: object,
|
||||
) -> Iterator[Tag | None]:
|
||||
"""Create a logical map context.
|
||||
|
||||
Dear PyGui item creation is added in a later rebuild step; this context currently
|
||||
registers thread-safe logical state so overlays can be declared in context.
|
||||
"""
|
||||
"""Create a Dear PyGui child-window map shell and logical map context."""
|
||||
|
||||
cache_dir_value = kwargs.get("cache_dir")
|
||||
cache_dir = cache_dir_value if isinstance(cache_dir_value, str | Path) else None
|
||||
@@ -46,5 +44,29 @@ def map_widget(
|
||||
cache_dir=cache_dir,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
child_kwargs: dict[str, Any] = dict(kwargs)
|
||||
child_kwargs.pop("cache_dir", None)
|
||||
child_kwargs.pop("user_agent", None)
|
||||
child_kwargs.setdefault("border", False)
|
||||
child_kwargs.setdefault("no_scrollbar", True)
|
||||
child_kwargs.setdefault("no_scroll_with_mouse", True)
|
||||
|
||||
dpg.add_child_window(
|
||||
tag=state.child_window_tag,
|
||||
width=width,
|
||||
height=height,
|
||||
autosize_x=autosize_x,
|
||||
autosize_y=autosize_y,
|
||||
**child_kwargs,
|
||||
)
|
||||
dpg.add_drawlist(1, 1, tag=state.drawlist_tag, parent=state.child_window_tag)
|
||||
|
||||
renderer = MapRenderer(state, dpg)
|
||||
with state.lock:
|
||||
state.renderer = renderer
|
||||
renderer.schedule_next_frame()
|
||||
|
||||
with current_map_context(state.tag):
|
||||
yield state.tag
|
||||
|
||||
19
tests/test_interaction.py
Normal file
19
tests/test_interaction.py
Normal file
@@ -0,0 +1,19 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dpg_map.interaction import calculate_hit_rect
|
||||
from dpg_map.sizing import SizeMeasurement, apply_size_measurement
|
||||
from dpg_map.state import create_map_state
|
||||
|
||||
|
||||
def test_hit_rect_uses_effective_map_size() -> None:
|
||||
state = create_map_state(tag="hit-rect")
|
||||
apply_size_measurement(state, SizeMeasurement(width=400, height=250, visible=True))
|
||||
|
||||
rect = calculate_hit_rect(state, (10.0, 20.0))
|
||||
|
||||
assert rect.x == 10.0
|
||||
assert rect.y == 20.0
|
||||
assert rect.width == 400
|
||||
assert rect.height == 250
|
||||
assert rect.contains(410.0, 270.0)
|
||||
assert not rect.contains(411.0, 270.0)
|
||||
30
tests/test_renderer.py
Normal file
30
tests/test_renderer.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dpg_map.commands import CommandKind, MapCommand
|
||||
from dpg_map.renderer import drain_renderer_commands
|
||||
from dpg_map.state import DirtyFlags, create_map_state
|
||||
|
||||
|
||||
def test_renderer_command_drain_preserves_structural_order_and_coalesces() -> None:
|
||||
state = create_map_state(tag="renderer-drain")
|
||||
state.dirty = DirtyFlags.NONE
|
||||
|
||||
state.command_queue.put(MapCommand(CommandKind.ADD_OVERLAY, state.tag, {"tag": "a"}))
|
||||
state.command_queue.put(MapCommand(CommandKind.SET_VIEW, state.tag, {"zoom": 3}))
|
||||
state.command_queue.put(MapCommand(CommandKind.SET_VIEW, state.tag, {"zoom": 4}))
|
||||
state.command_queue.put(MapCommand(CommandKind.UPDATE_OVERLAY, state.tag, {"tag": "a", "v": 1}))
|
||||
state.command_queue.put(MapCommand(CommandKind.UPDATE_OVERLAY, state.tag, {"tag": "a", "v": 2}))
|
||||
state.command_queue.put(MapCommand(CommandKind.DELETE_OVERLAY, state.tag, {"tag": "a"}))
|
||||
|
||||
commands = drain_renderer_commands(state)
|
||||
|
||||
assert [command.kind for command in commands] == [
|
||||
CommandKind.ADD_OVERLAY,
|
||||
CommandKind.SET_VIEW,
|
||||
CommandKind.UPDATE_OVERLAY,
|
||||
CommandKind.DELETE_OVERLAY,
|
||||
]
|
||||
assert commands[1].payload == {"zoom": 4}
|
||||
assert commands[2].payload == {"tag": "a", "v": 2}
|
||||
assert state.dirty & DirtyFlags.VIEW
|
||||
assert state.dirty & DirtyFlags.OVERLAYS
|
||||
44
tests/test_sizing.py
Normal file
44
tests/test_sizing.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dpg_map.sizing import SizeMeasurement, apply_size_measurement, effective_draw_size
|
||||
from dpg_map.state import DirtyFlags, create_map_state
|
||||
|
||||
|
||||
def test_size_measurement_tracks_last_nonzero_size() -> None:
|
||||
state = create_map_state(tag="size-last-nonzero")
|
||||
|
||||
first = apply_size_measurement(state, SizeMeasurement(width=640, height=360, visible=True))
|
||||
hidden = apply_size_measurement(state, SizeMeasurement(width=0, height=0, visible=False))
|
||||
|
||||
assert first.changed is True
|
||||
assert state.measured_width == 0
|
||||
assert state.measured_height == 0
|
||||
assert state.last_nonzero_width == 640
|
||||
assert state.last_nonzero_height == 360
|
||||
assert hidden.became_hidden is True
|
||||
assert effective_draw_size(state) == (640, 360)
|
||||
|
||||
|
||||
def test_zero_size_does_not_permanently_collapse_map() -> None:
|
||||
state = create_map_state(tag="size-reappears")
|
||||
|
||||
apply_size_measurement(state, SizeMeasurement(width=500, height=300, visible=True))
|
||||
apply_size_measurement(state, SizeMeasurement(width=0, height=0, visible=False))
|
||||
update = apply_size_measurement(state, SizeMeasurement(width=700, height=450, visible=True))
|
||||
|
||||
assert update.became_visible is True
|
||||
assert update.effective_width == 700
|
||||
assert update.effective_height == 450
|
||||
assert state.last_nonzero_width == 700
|
||||
assert state.last_nonzero_height == 450
|
||||
|
||||
|
||||
def test_resize_marks_size_dirty() -> None:
|
||||
state = create_map_state(tag="size-dirty")
|
||||
state.dirty = DirtyFlags.NONE
|
||||
|
||||
apply_size_measurement(state, SizeMeasurement(width=320, height=240, visible=True))
|
||||
|
||||
assert state.dirty & DirtyFlags.SIZE
|
||||
assert state.dirty & DirtyFlags.TILES
|
||||
assert state.dirty & DirtyFlags.OVERLAYS
|
||||
Reference in New Issue
Block a user