Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 72b6e3714f | |||
| c95ed3a239 | |||
| a8a6e1db19 | |||
| 5b1826d9c3 | |||
| 58ffcb8fb9 | |||
| 0a7b4f0172 | |||
| 5bd819d812 | |||
| 44403bea2b | |||
| accd1d1dee | |||
| ceba71927d | |||
| fe6670af9e | |||
| ac678e8dce | |||
| 63e79ebf8d |
19
README.md
19
README.md
@@ -37,12 +37,29 @@ dpg.destroy_context()
|
||||
```python
|
||||
with dpg.window(label="Engine"):
|
||||
with dpgg.gauge_panel(label="Powertrain", width=-1, height=260):
|
||||
with dpgg.gauge_grid(columns=3):
|
||||
with dpgg.gauge_grid(columns=3, min_column_width=180):
|
||||
dpgg.analog_gauge(tag="rpm", label="RPM", max_value=8000, redline_start=6500)
|
||||
dpgg.bar_gauge(tag="boost", label="Boost", max_value=30, unit="psi")
|
||||
dpgg.status_light(tag="ecu", label="ECU", state=True)
|
||||
```
|
||||
|
||||
`gauge_grid` is a wrapping, table-backed layout. An integer `columns` value is a maximum; if the
|
||||
parent is too narrow for `columns * min_column_width`, the grid uses fewer columns and continues on
|
||||
the next row instead of creating a horizontal scrollbar. Use `columns="auto"` for autobreak mode,
|
||||
where the grid derives the column count from the available width. Gauges with `width=0` or
|
||||
`width=-1` fill their grid cell.
|
||||
|
||||
Gauges also accept Dear PyGui's usual `parent` argument for adding a gauge to an
|
||||
existing container after that container has already been created:
|
||||
|
||||
```python
|
||||
dpg.add_window(tag="data_input", label="Data Input")
|
||||
dpgg.analog_gauge(tag="rpm", parent="data_input", label="RPM")
|
||||
```
|
||||
|
||||
The `parent` is applied to the gauge's outer group. The gauge's internal drawlist
|
||||
is then parented to that group.
|
||||
|
||||
## GUI-Thread Contract
|
||||
|
||||
Dear PyGui item operations must happen on the GUI thread. Gauge creation and runtime updates such as
|
||||
|
||||
@@ -24,7 +24,14 @@ Creation functions are context managers and accept common options such as `tag`,
|
||||
## Layout Helpers
|
||||
|
||||
- `gauge_panel(**config)`: child-window panel for grouped gauges.
|
||||
- `gauge_grid(**config)`: lightweight horizontal gauge group.
|
||||
- `gauge_grid(columns=1, min_column_width=180, fit_gauges=True, cell_padding=12, **config)`:
|
||||
table-backed
|
||||
wrapping gauge grid. Pass an integer `columns` to set the maximum column count; if the parent is
|
||||
narrower than `columns * min_column_width`, the grid uses fewer columns and wraps gauges onto
|
||||
later rows to avoid horizontal scrolling. Pass `columns="auto"` for autobreak mode, where the
|
||||
column count is derived entirely from the available width and `min_column_width`. Gauges without
|
||||
an explicit positive width fill their grid cell. Increase `cell_padding` if a custom Dear PyGui
|
||||
theme uses wider table padding or borders.
|
||||
|
||||
## Runtime API
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Run examples with `uv run python examples/<name>.py`.
|
||||
- `thresholds_markers.py`
|
||||
- `animations.py`
|
||||
- `dashboard.py`
|
||||
- `layout_grid.py`
|
||||
- `sizing.py`
|
||||
- `live_high_rate.py`
|
||||
- `live_worst_case.py`
|
||||
|
||||
91
examples/autobreak_runtime.py
Normal file
91
examples/autobreak_runtime.py
Normal file
@@ -0,0 +1,91 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
import dpg_gauges as dpgg
|
||||
|
||||
COUNTERS: dict[str, int] = {}
|
||||
|
||||
|
||||
def add_runtime_gauges(grid: str, prefix: str, count: int = 6) -> None:
|
||||
start = COUNTERS.get(grid, 0)
|
||||
for offset in range(count):
|
||||
index = start + offset
|
||||
value = (index * 17 + 23) % 101
|
||||
dpgg.digital_gauge(
|
||||
tag=f"{prefix}_{index}",
|
||||
parent=grid,
|
||||
label=f"{prefix} {index + 1}",
|
||||
value=value,
|
||||
unit="%",
|
||||
height=72,
|
||||
)
|
||||
COUNTERS[grid] = start + count
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
try:
|
||||
with dpg.window(label="Runtime autobreak tests", width=1220, height=900):
|
||||
dpg.add_text("Fixed panel, runtime gauges: expected 5 columns at width=1000")
|
||||
with dpgg.gauge_panel(label="Fixed 1000 x 230", width=1000, height=230):
|
||||
dpgg.gauge_grid(tag="fixed_runtime_grid", columns="auto", min_column_width=180)
|
||||
dpg.add_button(
|
||||
label="Add fixed gauges",
|
||||
callback=lambda: add_runtime_gauges("fixed_runtime_grid", "fixed"),
|
||||
)
|
||||
|
||||
dpg.add_spacer(height=10)
|
||||
dpg.add_text("Autosize panel with configured size: should match fixed panel")
|
||||
with dpgg.gauge_panel(
|
||||
label="Autosize configured 1000 x 230",
|
||||
width=1000,
|
||||
height=230,
|
||||
autosize_x=True,
|
||||
autosize_y=True,
|
||||
):
|
||||
dpgg.gauge_grid(tag="autosize_runtime_grid", columns="auto", min_column_width=180)
|
||||
dpg.add_button(
|
||||
label="Add autosize gauges",
|
||||
callback=lambda: add_runtime_gauges("autosize_runtime_grid", "auto"),
|
||||
)
|
||||
|
||||
dpg.add_spacer(height=10)
|
||||
dpg.add_text("Autosize panel without width: grid falls back to the owning window width")
|
||||
with dpgg.gauge_panel(label="Autosize inherited width", height=230, autosize_x=True):
|
||||
dpgg.gauge_grid(tag="inherited_runtime_grid", columns="auto", min_column_width=180)
|
||||
dpg.add_button(
|
||||
label="Add inherited-width gauges",
|
||||
callback=lambda: add_runtime_gauges("inherited_runtime_grid", "inherited"),
|
||||
)
|
||||
|
||||
dpg.add_spacer(height=10)
|
||||
dpg.add_text("Narrow autosize panel: expected 2 columns at width=430")
|
||||
with dpgg.gauge_panel(
|
||||
label="Narrow autosize 430 x 210",
|
||||
width=430,
|
||||
height=210,
|
||||
autosize_x=True,
|
||||
autosize_y=True,
|
||||
):
|
||||
dpgg.gauge_grid(tag="narrow_runtime_grid", columns="auto", min_column_width=180)
|
||||
dpg.add_button(
|
||||
label="Add narrow gauges",
|
||||
callback=lambda: add_runtime_gauges("narrow_runtime_grid", "narrow"),
|
||||
)
|
||||
|
||||
dpg.set_frame_callback(2, lambda: add_runtime_gauges("fixed_runtime_grid", "fixed"))
|
||||
dpg.set_frame_callback(3, lambda: add_runtime_gauges("autosize_runtime_grid", "auto"))
|
||||
dpg.set_frame_callback(4, lambda: add_runtime_gauges("inherited_runtime_grid", "inherited"))
|
||||
dpg.set_frame_callback(5, lambda: add_runtime_gauges("narrow_runtime_grid", "narrow"))
|
||||
|
||||
dpg.create_viewport(title="dpg-gauges runtime autobreak", width=1260, height=940)
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
finally:
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
65
examples/layout_grid.py
Normal file
65
examples/layout_grid.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
import dpg_gauges as dpgg
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
try:
|
||||
with dpg.window(label="Grid layout", width=820, height=620):
|
||||
dpg.add_text("Max columns: columns=3 wraps onto new rows after every third gauge")
|
||||
with dpgg.gauge_panel(label="Wide panel", width=-1, height=270), dpgg.gauge_grid(
|
||||
columns=3, min_column_width=190
|
||||
):
|
||||
dpgg.analog_gauge(
|
||||
tag="grid_rpm", label="RPM", value=4200, max_value=8000, height=190
|
||||
)
|
||||
dpgg.digital_gauge(
|
||||
tag="grid_speed", label="Speed", value=88, unit="km/h", height=90
|
||||
)
|
||||
dpgg.bar_gauge(
|
||||
tag="grid_boost", label="Boost", value=12.5, max_value=30, unit="psi"
|
||||
)
|
||||
dpgg.status_light(tag="grid_ecu", label="ECU", state=True, height=70)
|
||||
dpgg.battery_gauge(tag="grid_battery", label="Battery", value=74, height=160)
|
||||
|
||||
dpg.add_spacer(height=8)
|
||||
dpg.add_text("Autobreak: columns='auto' chooses columns from available width")
|
||||
with (
|
||||
dpgg.gauge_panel(label="Autobreak narrow panel", width=430, height=285),
|
||||
dpgg.gauge_grid(columns="auto", min_column_width=180),
|
||||
):
|
||||
for index, value in enumerate((18, 42, 67, 91, 55), start=1):
|
||||
dpgg.digital_gauge(
|
||||
tag=f"narrow_{index}",
|
||||
label=f"Metric {index}",
|
||||
value=value,
|
||||
unit="%",
|
||||
height=72,
|
||||
)
|
||||
|
||||
dpg.add_spacer(height=8)
|
||||
dpg.add_text("Panels can also occupy grid cells")
|
||||
with dpgg.gauge_grid(columns=2, min_column_width=240):
|
||||
with dpgg.gauge_panel(label="Left cell", height=120):
|
||||
dpgg.segmented_gauge(tag="left_segments", label="Load", value=70, height=80)
|
||||
with dpgg.gauge_panel(label="Right cell", height=120):
|
||||
dpgg.multi_value_gauge(
|
||||
tag="right_values",
|
||||
label="Sensors",
|
||||
values={"oil": 82, "fuel": 64, "air": 31},
|
||||
height=92,
|
||||
)
|
||||
|
||||
dpg.create_viewport(title="dpg-gauges grid layout", width=860, height=660)
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
finally:
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
62
examples/reconnect_grid.py
Normal file
62
examples/reconnect_grid.py
Normal file
@@ -0,0 +1,62 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
import dpg_gauges as dpgg
|
||||
|
||||
GRID = "connector_grid"
|
||||
ACTIVE_TAGS: list[str] = []
|
||||
CYCLE = 0
|
||||
|
||||
|
||||
def connect_connector() -> None:
|
||||
global CYCLE
|
||||
if ACTIVE_TAGS:
|
||||
return
|
||||
for index in range(5):
|
||||
tag = f"connector_{CYCLE}_{index}"
|
||||
ACTIVE_TAGS.append(tag)
|
||||
dpgg.digital_gauge(
|
||||
tag=tag,
|
||||
parent=GRID,
|
||||
label=f"Conn {CYCLE}.{index}",
|
||||
value=(CYCLE * 13 + index * 17) % 100,
|
||||
unit="%",
|
||||
height=72,
|
||||
)
|
||||
CYCLE += 1
|
||||
|
||||
|
||||
def disconnect_connector() -> None:
|
||||
while ACTIVE_TAGS:
|
||||
dpgg.delete_gauge(ACTIVE_TAGS.pop())
|
||||
|
||||
|
||||
def reconnect_connector() -> None:
|
||||
disconnect_connector()
|
||||
connect_connector()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
dpg.create_context()
|
||||
try:
|
||||
with dpg.window(label="Reconnect grid test", width=860, height=420):
|
||||
dpg.add_text("Reconnect should always start at the first grid cell.")
|
||||
with dpgg.gauge_panel(label="Connector gauges", width=820, height=230):
|
||||
dpgg.gauge_grid(tag=GRID, columns="auto", min_column_width=180)
|
||||
with dpg.group(horizontal=True):
|
||||
dpg.add_button(label="Connect", callback=connect_connector)
|
||||
dpg.add_button(label="Disconnect", callback=disconnect_connector)
|
||||
dpg.add_button(label="Reconnect", callback=reconnect_connector)
|
||||
|
||||
dpg.set_frame_callback(2, connect_connector)
|
||||
dpg.create_viewport(title="dpg-gauges reconnect grid", width=900, height=460)
|
||||
dpg.setup_dearpygui()
|
||||
dpg.show_viewport()
|
||||
dpg.start_dearpygui()
|
||||
finally:
|
||||
dpg.destroy_context()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "dpg-gauges"
|
||||
version = "1.0.0"
|
||||
version = "1.1.2"
|
||||
description = "Dear PyGui gauge widgets for dashboards and telemetry displays"
|
||||
readme = "README.md"
|
||||
authors = [
|
||||
|
||||
@@ -35,7 +35,7 @@ from .types import (
|
||||
ThresholdBand,
|
||||
)
|
||||
|
||||
__version__ = "1.0.0"
|
||||
__version__ = "1.1.2"
|
||||
|
||||
__all__ = [
|
||||
"configure",
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from . import widget as _widget
|
||||
from .gauges import GaugeConfig
|
||||
from .state import (
|
||||
configure_gauges,
|
||||
@@ -17,6 +16,44 @@ from .state import (
|
||||
update_runtime_gauge,
|
||||
)
|
||||
from .types import GaugeMarker, GaugeValue, Tag, ThresholdBand
|
||||
from .widget import (
|
||||
analog_gauge,
|
||||
bar_gauge,
|
||||
battery_gauge,
|
||||
compass_gauge,
|
||||
digital_gauge,
|
||||
gauge_grid,
|
||||
gauge_panel,
|
||||
level_gauge,
|
||||
multi_value_gauge,
|
||||
segmented_gauge,
|
||||
status_light,
|
||||
thermometer_gauge,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"analog_gauge",
|
||||
"bar_gauge",
|
||||
"battery_gauge",
|
||||
"compass_gauge",
|
||||
"configure",
|
||||
"configure_gauge",
|
||||
"delete_gauge",
|
||||
"digital_gauge",
|
||||
"gauge_grid",
|
||||
"gauge_panel",
|
||||
"get_value",
|
||||
"level_gauge",
|
||||
"multi_value_gauge",
|
||||
"segmented_gauge",
|
||||
"set_markers",
|
||||
"set_show",
|
||||
"set_thresholds",
|
||||
"set_value",
|
||||
"status_light",
|
||||
"thermometer_gauge",
|
||||
"update_gauge",
|
||||
]
|
||||
|
||||
|
||||
def configure(**config: Any) -> None:
|
||||
@@ -24,20 +61,6 @@ def configure(**config: Any) -> None:
|
||||
configure_gauges(**config)
|
||||
|
||||
|
||||
analog_gauge = _widget.analog_gauge
|
||||
digital_gauge = _widget.digital_gauge
|
||||
bar_gauge = _widget.bar_gauge
|
||||
level_gauge = _widget.level_gauge
|
||||
status_light = _widget.status_light
|
||||
segmented_gauge = _widget.segmented_gauge
|
||||
compass_gauge = _widget.compass_gauge
|
||||
thermometer_gauge = _widget.thermometer_gauge
|
||||
battery_gauge = _widget.battery_gauge
|
||||
multi_value_gauge = _widget.multi_value_gauge
|
||||
gauge_panel = _widget.gauge_panel
|
||||
gauge_grid = _widget.gauge_grid
|
||||
|
||||
|
||||
def set_value(tag: Tag, value: GaugeValue) -> None:
|
||||
"""Set a gauge target value from the Dear PyGui GUI thread."""
|
||||
set_runtime_value(tag, value)
|
||||
@@ -78,4 +101,4 @@ def delete_gauge(tag: Tag) -> None:
|
||||
delete_runtime_gauge(tag)
|
||||
|
||||
|
||||
__all_configs__ = (GaugeConfig,)
|
||||
__all_configs__: tuple[type[GaugeConfig], ...] = (GaugeConfig,)
|
||||
|
||||
@@ -36,6 +36,7 @@ def _normalize_smoothing(value: SmoothingConfig | bool | None) -> SmoothingConfi
|
||||
@dataclass(frozen=True)
|
||||
class GaugeConfig:
|
||||
tag: Tag | None = None
|
||||
parent: Tag | None = None
|
||||
label: str | None = None
|
||||
value: GaugeValue = None
|
||||
min_value: float = 0.0
|
||||
|
||||
0
src/dpg_gauges/py.typed
Normal file
0
src/dpg_gauges/py.typed
Normal file
@@ -50,7 +50,7 @@ class GaugeRenderer:
|
||||
|
||||
def __init__(self, state: GaugeState) -> None:
|
||||
self.state = state
|
||||
self.layers = DrawLayers()
|
||||
self.layers: DrawLayers = DrawLayers()
|
||||
self._scheduled = False
|
||||
|
||||
def render(self) -> None:
|
||||
@@ -62,9 +62,12 @@ class GaugeRenderer:
|
||||
if state.animation_state.active or state.smoothing_state.active:
|
||||
self.schedule_frame()
|
||||
|
||||
def delete(self) -> None:
|
||||
if dpg.does_item_exist(self.state.container_tag):
|
||||
dpg.delete_item(self.state.container_tag)
|
||||
def delete(self) -> Tag | None:
|
||||
if not dpg.does_item_exist(self.state.container_tag):
|
||||
return None
|
||||
parent = dpg.get_item_parent(self.state.container_tag)
|
||||
dpg.delete_item(self.state.container_tag)
|
||||
return cast(Tag | None, parent)
|
||||
|
||||
def schedule_frame(self) -> None:
|
||||
if self._scheduled:
|
||||
|
||||
@@ -301,9 +301,14 @@ def set_runtime_show(tag: Tag, show: bool) -> None:
|
||||
|
||||
def delete_runtime_gauge(tag: Tag) -> None:
|
||||
state = get_gauge_state(tag)
|
||||
layout_parent: Tag | None = None
|
||||
if state.renderer is not None:
|
||||
state.renderer.delete()
|
||||
layout_parent = state.renderer.delete()
|
||||
del _registry[tag]
|
||||
if layout_parent is not None:
|
||||
from .widget import _cleanup_grid_after_item_delete
|
||||
|
||||
_cleanup_grid_after_item_delete(layout_parent)
|
||||
|
||||
|
||||
def render_runtime_gauge(state: GaugeState) -> None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, replace
|
||||
from types import TracebackType
|
||||
from typing import Any, Generic, Literal, TypeVar, cast
|
||||
|
||||
@@ -42,6 +42,11 @@ ConfigT = TypeVar("ConfigT", bound=GaugeConfig)
|
||||
_dpg_context_active = False
|
||||
_original_create_context = dpg.create_context
|
||||
_original_destroy_context = dpg.destroy_context
|
||||
_active_grids: list[GridLayoutContext] = []
|
||||
_grid_layouts: dict[Tag, GridLayoutContext] = {}
|
||||
_pending_grid_cleanups: set[Tag] = set()
|
||||
_layout_suspend_depth = 0
|
||||
_grid_refresh_scheduled = False
|
||||
|
||||
|
||||
def _install_context_tracking() -> None:
|
||||
@@ -55,11 +60,15 @@ def _install_context_tracking() -> None:
|
||||
return result
|
||||
|
||||
def destroy_context(*args: Any, **kwargs: Any) -> Any:
|
||||
global _dpg_context_active
|
||||
global _dpg_context_active, _grid_refresh_scheduled
|
||||
try:
|
||||
return _original_destroy_context(*args, **kwargs)
|
||||
finally:
|
||||
_dpg_context_active = False
|
||||
_active_grids.clear()
|
||||
_grid_layouts.clear()
|
||||
_pending_grid_cleanups.clear()
|
||||
_grid_refresh_scheduled = False
|
||||
|
||||
create_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
|
||||
destroy_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
|
||||
@@ -73,10 +82,14 @@ _install_context_tracking()
|
||||
@dataclass
|
||||
class GaugeContext(Generic[ConfigT]):
|
||||
state: GaugeState
|
||||
_suspended_layout: bool = False
|
||||
|
||||
def __enter__(self) -> ConfigT:
|
||||
global _layout_suspend_depth
|
||||
if _dpg_context_active and dpg.does_item_exist(self.state.container_tag):
|
||||
dpg.push_container_stack(self.state.container_tag)
|
||||
_layout_suspend_depth += 1
|
||||
self._suspended_layout = True
|
||||
return cast(ConfigT, self.state.config)
|
||||
|
||||
def __exit__(
|
||||
@@ -85,17 +98,27 @@ class GaugeContext(Generic[ConfigT]):
|
||||
exc: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
global _layout_suspend_depth
|
||||
if _dpg_context_active and dpg.does_item_exist(self.state.container_tag):
|
||||
dpg.pop_container_stack()
|
||||
if self._suspended_layout:
|
||||
_layout_suspend_depth = max(_layout_suspend_depth - 1, 0)
|
||||
self._suspended_layout = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class LayoutContext:
|
||||
tag: Tag | None
|
||||
suspend_layout: bool = True
|
||||
_suspended_layout: bool = False
|
||||
|
||||
def __enter__(self) -> Tag | None:
|
||||
global _layout_suspend_depth
|
||||
if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag):
|
||||
dpg.push_container_stack(self.tag)
|
||||
if self.suspend_layout:
|
||||
_layout_suspend_depth += 1
|
||||
self._suspended_layout = True
|
||||
return self.tag
|
||||
|
||||
def __exit__(
|
||||
@@ -104,14 +127,104 @@ class LayoutContext:
|
||||
exc: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
global _layout_suspend_depth
|
||||
if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag):
|
||||
dpg.pop_container_stack()
|
||||
if self._suspended_layout:
|
||||
_layout_suspend_depth = max(_layout_suspend_depth - 1, 0)
|
||||
self._suspended_layout = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class GridLayoutContext:
|
||||
tag: Tag | None
|
||||
parent: Tag | None
|
||||
width: int
|
||||
requested_columns: int | Literal["auto"]
|
||||
columns: int
|
||||
min_column_width: int
|
||||
fit_gauges: bool
|
||||
cell_padding: int = 12
|
||||
row_height: int = 0
|
||||
available_width: int = 0
|
||||
_next_index: int = 0
|
||||
_current_row: Tag | None = None
|
||||
_current_row_cells: int = 0
|
||||
_suspend_depth: int = 0
|
||||
|
||||
def __enter__(self) -> Tag | None:
|
||||
self._suspend_depth = _layout_suspend_depth
|
||||
if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag):
|
||||
_active_grids.append(self)
|
||||
return self.tag
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
if _active_grids and _active_grids[-1] is self:
|
||||
_active_grids.pop()
|
||||
elif self in _active_grids:
|
||||
_active_grids.remove(self)
|
||||
|
||||
def next_cell(self) -> tuple[Tag | None, int | None]:
|
||||
if self.tag is None or not _dpg_context_active or not dpg.does_item_exist(self.tag):
|
||||
return None, None
|
||||
self.refresh_columns()
|
||||
empty_cell = _first_empty_table_cell(self.tag)
|
||||
if empty_cell is not None:
|
||||
self._next_index = _table_cell_count(self.tag)
|
||||
self._current_row, self._current_row_cells = _last_row_state(
|
||||
self.tag, self.columns
|
||||
)
|
||||
return empty_cell, self.cell_width()
|
||||
if self._current_row is None or self._current_row_cells >= self.columns:
|
||||
self._current_row = dpg.add_table_row(parent=self.tag, height=self.row_height)
|
||||
self._current_row_cells = 0
|
||||
cell = dpg.add_table_cell(parent=self._current_row or self.tag)
|
||||
self._next_index += 1
|
||||
self._current_row_cells += 1
|
||||
return cell, self.cell_width()
|
||||
|
||||
def cell_width(self) -> int | None:
|
||||
if self.available_width <= 0:
|
||||
return None
|
||||
return max(int(self.available_width / self.columns) - self.cell_padding, 1)
|
||||
|
||||
def refresh_columns(self) -> None:
|
||||
if self.tag is None or not _dpg_context_active or not dpg.does_item_exist(self.tag):
|
||||
return
|
||||
available_width = _available_layout_width(self.parent, self.width)
|
||||
if available_width <= 0:
|
||||
return
|
||||
columns = _resolved_columns(self.requested_columns, self.min_column_width, available_width)
|
||||
if columns == self.columns and available_width == self.available_width:
|
||||
return
|
||||
_reflow_table_cells(self.tag, columns, self.row_height)
|
||||
self.columns = columns
|
||||
self.available_width = available_width
|
||||
self._next_index = _table_cell_count(self.tag)
|
||||
self._current_row, self._current_row_cells = _last_row_state(self.tag, self.columns)
|
||||
|
||||
|
||||
def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[ConfigT]:
|
||||
if not _dpg_context_active:
|
||||
return GaugeContext(register_gauge(gauge_type, config))
|
||||
|
||||
layout_parent, layout_width, layout_grid = _next_layout_cell(config.parent)
|
||||
if layout_parent is not None:
|
||||
updates: dict[str, Any] = {"parent": layout_parent}
|
||||
if (
|
||||
layout_width is not None
|
||||
and layout_grid is not None
|
||||
and layout_grid.fit_gauges
|
||||
and config.width <= 0
|
||||
):
|
||||
updates["width"] = layout_width
|
||||
config = replace(config, **updates)
|
||||
|
||||
tag = config.tag
|
||||
container_tag = tag if tag is not None else None
|
||||
drawlist_tag = f"{tag}__drawlist" if tag is not None else None
|
||||
@@ -122,6 +235,7 @@ def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[Confi
|
||||
|
||||
container = dpg.add_group(
|
||||
tag=container_tag or 0,
|
||||
parent=config.parent or 0,
|
||||
width=width,
|
||||
height=height,
|
||||
show=config.show,
|
||||
@@ -150,9 +264,239 @@ def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[Confi
|
||||
return GaugeContext(state)
|
||||
|
||||
|
||||
def _next_layout_cell(
|
||||
parent: Tag | None,
|
||||
) -> tuple[Tag | None, int | None, GridLayoutContext | None]:
|
||||
if parent is not None:
|
||||
grid = _grid_layouts.get(parent)
|
||||
if grid is not None:
|
||||
cell, width = grid.next_cell()
|
||||
return cell, width, grid
|
||||
return None, None, None
|
||||
|
||||
grid = _current_grid()
|
||||
if grid is None:
|
||||
return None, None, None
|
||||
cell, width = grid.next_cell()
|
||||
return cell, width, grid
|
||||
|
||||
|
||||
def _current_grid() -> GridLayoutContext | None:
|
||||
if not _active_grids:
|
||||
return None
|
||||
grid = _active_grids[-1]
|
||||
if grid._suspend_depth != _layout_suspend_depth:
|
||||
return None
|
||||
return grid
|
||||
|
||||
|
||||
def _available_layout_width(parent: Tag | None, width: int) -> int:
|
||||
if width > 0:
|
||||
return width
|
||||
if parent is not None and dpg.does_item_exist(parent):
|
||||
return _available_layout_width_for_item(parent)
|
||||
try:
|
||||
container = dpg.top_container_stack()
|
||||
except Exception:
|
||||
return 0
|
||||
if container is not None and dpg.does_item_exist(container):
|
||||
return _available_layout_width_for_item(container)
|
||||
return 0
|
||||
|
||||
|
||||
def _available_layout_width_for_item(item: Tag) -> int:
|
||||
while item is not None and dpg.does_item_exist(item):
|
||||
width = _item_layout_width(item)
|
||||
if width > 0:
|
||||
return width
|
||||
parent = dpg.get_item_parent(item)
|
||||
if parent is None or parent == item:
|
||||
break
|
||||
item = parent
|
||||
return 0
|
||||
|
||||
|
||||
def _item_layout_width(item: Tag) -> int:
|
||||
width = max(dpg.get_item_width(item) or 0, 0)
|
||||
if width > 0:
|
||||
return width
|
||||
try:
|
||||
configured_width = dpg.get_item_configuration(item).get("width", 0)
|
||||
except Exception:
|
||||
return 0
|
||||
if isinstance(configured_width, int | float) and configured_width > 0:
|
||||
return int(configured_width)
|
||||
return 0
|
||||
|
||||
|
||||
def _set_table_column_count(table: Tag, columns: int) -> None:
|
||||
children = cast(dict[int, list[Tag]], dpg.get_item_children(table) or {})
|
||||
table_columns = list(children.get(0, []))
|
||||
current = len(table_columns)
|
||||
if columns > current:
|
||||
for _ in range(columns - current):
|
||||
dpg.add_table_column(parent=table, width_stretch=True, init_width_or_weight=1.0)
|
||||
elif columns < current:
|
||||
for column in reversed(table_columns[columns:]):
|
||||
dpg.delete_item(column)
|
||||
|
||||
|
||||
def _reflow_table_cells(table: Tag, columns: int, row_height: int = 0) -> None:
|
||||
columns = max(int(columns), 1)
|
||||
cell_contents = _table_cell_contents(table)
|
||||
old_rows = _table_rows(table)
|
||||
if not cell_contents:
|
||||
for row in old_rows:
|
||||
if dpg.does_item_exist(row):
|
||||
dpg.delete_item(row)
|
||||
_set_table_column_count(table, columns)
|
||||
return
|
||||
|
||||
if columns > len(_table_columns(table)):
|
||||
_set_table_column_count(table, columns)
|
||||
|
||||
new_rows: list[Tag] = []
|
||||
for index, contents in enumerate(cell_contents):
|
||||
if index % columns == 0:
|
||||
new_rows.append(dpg.add_table_row(parent=table, height=row_height))
|
||||
cell = dpg.add_table_cell(parent=new_rows[-1])
|
||||
for child in contents:
|
||||
if dpg.does_item_exist(child):
|
||||
dpg.move_item(child, parent=cell)
|
||||
|
||||
for row in old_rows:
|
||||
if dpg.does_item_exist(row):
|
||||
dpg.delete_item(row)
|
||||
|
||||
if columns < len(_table_columns(table)):
|
||||
_set_table_column_count(table, columns)
|
||||
|
||||
|
||||
def _table_columns(table: Tag) -> list[Tag]:
|
||||
children = cast(dict[int, list[Tag]], dpg.get_item_children(table) or {})
|
||||
return list(children.get(0, []))
|
||||
|
||||
|
||||
def _table_rows(table: Tag) -> list[Tag]:
|
||||
children = cast(dict[int, list[Tag]], dpg.get_item_children(table) or {})
|
||||
return list(children.get(1, []))
|
||||
|
||||
|
||||
def _table_cell_contents(table: Tag) -> list[list[Tag]]:
|
||||
cells: list[list[Tag]] = []
|
||||
for row in _table_rows(table):
|
||||
row_children = cast(dict[int, list[Tag]], dpg.get_item_children(row) or {})
|
||||
for cell in row_children.get(1, []):
|
||||
cell_children = cast(dict[int, list[Tag]], dpg.get_item_children(cell) or {})
|
||||
contents = [child for child in cell_children.get(1, []) if dpg.does_item_exist(child)]
|
||||
if contents:
|
||||
cells.append(contents)
|
||||
return cells
|
||||
|
||||
|
||||
def _first_empty_table_cell(table: Tag) -> Tag | None:
|
||||
for row in _table_rows(table):
|
||||
row_children = cast(dict[int, list[Tag]], dpg.get_item_children(row) or {})
|
||||
for cell in row_children.get(1, []):
|
||||
cell_children = cast(dict[int, list[Tag]], dpg.get_item_children(cell) or {})
|
||||
if not any(dpg.does_item_exist(child) for child in cell_children.get(1, [])):
|
||||
return cell
|
||||
return None
|
||||
|
||||
|
||||
def _table_cell_count(table: Tag) -> int:
|
||||
count = 0
|
||||
for row in _table_rows(table):
|
||||
row_children = cast(dict[int, list[Tag]], dpg.get_item_children(row) or {})
|
||||
count += len(row_children.get(1, []))
|
||||
return count
|
||||
|
||||
|
||||
def _last_row_state(table: Tag, columns: int) -> tuple[Tag | None, int]:
|
||||
rows = _table_rows(table)
|
||||
if not rows:
|
||||
return None, 0
|
||||
row = rows[-1]
|
||||
row_children = cast(dict[int, list[Tag]], dpg.get_item_children(row) or {})
|
||||
return row, min(len(row_children.get(1, [])), columns)
|
||||
|
||||
|
||||
def _cleanup_grid_after_item_delete(parent: Tag) -> None:
|
||||
if not _dpg_context_active or not dpg.does_item_exist(parent):
|
||||
return
|
||||
row = dpg.get_item_parent(parent)
|
||||
if row is None or not dpg.does_item_exist(row):
|
||||
return
|
||||
table = dpg.get_item_parent(row)
|
||||
if table is None or table not in _grid_layouts or not dpg.does_item_exist(table):
|
||||
return
|
||||
_pending_grid_cleanups.add(table)
|
||||
_schedule_grid_refresh()
|
||||
|
||||
|
||||
def _compact_grid(grid: GridLayoutContext) -> None:
|
||||
table = grid.tag
|
||||
if table is None or not dpg.does_item_exist(table):
|
||||
return
|
||||
_reflow_table_cells(table, grid.columns, grid.row_height)
|
||||
grid._next_index = _table_cell_count(table)
|
||||
grid._current_row, grid._current_row_cells = _last_row_state(table, grid.columns)
|
||||
|
||||
|
||||
def _cleanup_grid_now(table: Tag) -> None:
|
||||
if table not in _grid_layouts or not dpg.does_item_exist(table):
|
||||
return
|
||||
grid = _grid_layouts[table]
|
||||
_compact_grid(grid)
|
||||
|
||||
|
||||
def _schedule_grid_refresh() -> None:
|
||||
global _grid_refresh_scheduled
|
||||
if _grid_refresh_scheduled or not _dpg_context_active:
|
||||
return
|
||||
try:
|
||||
dpg.set_frame_callback(dpg.get_frame_count() + 1, _refresh_grid_layouts)
|
||||
except Exception:
|
||||
return
|
||||
_grid_refresh_scheduled = True
|
||||
|
||||
|
||||
def _refresh_grid_layouts() -> None:
|
||||
global _grid_refresh_scheduled
|
||||
_grid_refresh_scheduled = False
|
||||
if not _dpg_context_active:
|
||||
return
|
||||
for tag, grid in list(_grid_layouts.items()):
|
||||
if tag is None or not dpg.does_item_exist(tag):
|
||||
del _grid_layouts[tag]
|
||||
_pending_grid_cleanups.discard(tag)
|
||||
continue
|
||||
if tag in _pending_grid_cleanups:
|
||||
_pending_grid_cleanups.discard(tag)
|
||||
_compact_grid(grid)
|
||||
grid.refresh_columns()
|
||||
if _grid_layouts:
|
||||
_schedule_grid_refresh()
|
||||
|
||||
|
||||
def _resolved_columns(
|
||||
columns: int | Literal["auto"], min_column_width: int, available_width: int
|
||||
) -> int:
|
||||
min_column_width = max(int(min_column_width), 0)
|
||||
if columns == "auto":
|
||||
if available_width > 0 and min_column_width > 0:
|
||||
return max(1, available_width // min_column_width)
|
||||
return 1
|
||||
columns = max(int(columns), 1)
|
||||
if available_width > 0 and min_column_width > 0:
|
||||
columns = min(columns, max(1, available_width // min_column_width))
|
||||
return columns
|
||||
|
||||
|
||||
def gauge_panel(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
@@ -166,8 +510,14 @@ def gauge_panel(
|
||||
"""Create a lightweight child-window container for related gauges."""
|
||||
if not _dpg_context_active:
|
||||
return LayoutContext(None)
|
||||
layout_parent, layout_width, _layout_grid = _next_layout_cell(parent)
|
||||
if layout_parent is not None:
|
||||
parent = layout_parent
|
||||
if width <= 0 and layout_width is not None:
|
||||
width = layout_width
|
||||
container = dpg.add_child_window(
|
||||
tag=tag or 0,
|
||||
parent=parent or 0,
|
||||
label=label or "",
|
||||
width=width,
|
||||
height=height,
|
||||
@@ -183,31 +533,73 @@ def gauge_panel(
|
||||
|
||||
def gauge_grid(
|
||||
*,
|
||||
columns: int = 1,
|
||||
columns: int | Literal["auto"] = 1,
|
||||
min_column_width: int = 180,
|
||||
fit_gauges: bool = True,
|
||||
cell_padding: int = 12,
|
||||
row_height: int = 0,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
show: bool = True,
|
||||
**config: Any,
|
||||
) -> LayoutContext:
|
||||
"""Create a lightweight horizontal gauge layout container."""
|
||||
) -> GridLayoutContext:
|
||||
"""Create a lightweight wrapping gauge layout container."""
|
||||
if not _dpg_context_active:
|
||||
return LayoutContext(None)
|
||||
columns = max(int(columns), 1)
|
||||
container = dpg.add_group(
|
||||
return GridLayoutContext(
|
||||
tag=None,
|
||||
parent=None,
|
||||
width=width,
|
||||
requested_columns=columns,
|
||||
columns=1,
|
||||
min_column_width=min_column_width,
|
||||
fit_gauges=fit_gauges,
|
||||
cell_padding=cell_padding,
|
||||
)
|
||||
layout_parent, layout_width, _layout_grid = _next_layout_cell(parent)
|
||||
if layout_parent is not None:
|
||||
parent = layout_parent
|
||||
if width <= 0 and layout_width is not None:
|
||||
width = layout_width
|
||||
available_width = _available_layout_width(parent, width)
|
||||
requested_columns = columns
|
||||
resolved_columns = _resolved_columns(columns, min_column_width, available_width)
|
||||
container = dpg.add_table(
|
||||
tag=tag or 0,
|
||||
parent=parent or 0,
|
||||
width=width,
|
||||
height=height,
|
||||
show=show,
|
||||
horizontal=columns > 1,
|
||||
header_row=False,
|
||||
policy=dpg.mvTable_SizingStretchSame,
|
||||
scrollX=False,
|
||||
**config,
|
||||
)
|
||||
return LayoutContext(container)
|
||||
for _ in range(resolved_columns):
|
||||
dpg.add_table_column(parent=container, width_stretch=True, init_width_or_weight=1.0)
|
||||
resolved_parent = cast(Tag | None, dpg.get_item_parent(container))
|
||||
context = GridLayoutContext(
|
||||
tag=container,
|
||||
parent=resolved_parent,
|
||||
width=width,
|
||||
requested_columns=requested_columns,
|
||||
columns=resolved_columns,
|
||||
min_column_width=min_column_width,
|
||||
fit_gauges=fit_gauges,
|
||||
cell_padding=cell_padding,
|
||||
row_height=row_height,
|
||||
available_width=available_width,
|
||||
)
|
||||
_grid_layouts[container] = context
|
||||
_schedule_grid_refresh()
|
||||
return context
|
||||
|
||||
|
||||
def analog_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -253,6 +645,7 @@ def analog_gauge(
|
||||
def digital_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -287,6 +680,7 @@ def digital_gauge(
|
||||
def bar_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -326,6 +720,7 @@ def bar_gauge(
|
||||
def level_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -365,6 +760,7 @@ def level_gauge(
|
||||
def status_light(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -403,6 +799,7 @@ def status_light(
|
||||
def segmented_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -443,6 +840,7 @@ def segmented_gauge(
|
||||
def compass_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -488,6 +886,7 @@ def compass_gauge(
|
||||
def thermometer_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -529,6 +928,7 @@ def thermometer_gauge(
|
||||
def battery_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
@@ -569,6 +969,7 @@ def battery_gauge(
|
||||
def multi_value_gauge(
|
||||
*,
|
||||
tag: Tag | None = None,
|
||||
parent: Tag | None = None,
|
||||
label: str | None = None,
|
||||
value: GaugeValue = None,
|
||||
min_value: float = 0.0,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from typing import cast
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
import pytest
|
||||
@@ -9,6 +10,21 @@ import dpg_gauges as dpgg
|
||||
from dpg_gauges.state import clear_registry, get_gauge_state
|
||||
|
||||
|
||||
def _children(item: int | str) -> dict[int, list[int | str]]:
|
||||
return cast(dict[int, list[int | str]], dpg.get_item_children(item))
|
||||
|
||||
|
||||
def _occupied_cell_counts(table: int | str) -> list[int]:
|
||||
counts: list[int] = []
|
||||
for row in _children(table)[1]:
|
||||
occupied = 0
|
||||
for cell in _children(row)[1]:
|
||||
if any(dpg.does_item_exist(child) for child in _children(cell)[1]):
|
||||
occupied += 1
|
||||
counts.append(occupied)
|
||||
return counts
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def dpg_context() -> Generator[None]:
|
||||
clear_registry()
|
||||
@@ -76,3 +92,175 @@ def test_panel_and_grid_are_lightweight_containers(dpg_context: None) -> None:
|
||||
assert dpg.does_item_exist("engine")
|
||||
assert dpg.does_item_exist("engine_grid")
|
||||
assert get_gauge_state("temp").renderer is not None
|
||||
|
||||
|
||||
def test_grid_columns_wrap_gauges_into_rows(dpg_context: None) -> None:
|
||||
with dpg.window(label="Dashboard", width=720), dpgg.gauge_grid(
|
||||
tag="wrap_grid", columns=3, min_column_width=1
|
||||
):
|
||||
for index in range(5):
|
||||
dpgg.digital_gauge(tag=f"metric_{index}", value=index, height=60)
|
||||
|
||||
children = _children("wrap_grid")
|
||||
assert len(children[0]) == 3
|
||||
assert len(children[1]) == 2
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [3, 2]
|
||||
|
||||
|
||||
def test_grid_auto_reduces_columns_to_parent_width(dpg_context: None) -> None:
|
||||
with dpg.window(label="Narrow", width=380), dpgg.gauge_grid(
|
||||
tag="narrow_grid", columns=3, min_column_width=180
|
||||
):
|
||||
for index in range(5):
|
||||
dpgg.digital_gauge(tag=f"narrow_metric_{index}", value=index, height=60)
|
||||
|
||||
children = _children("narrow_grid")
|
||||
assert len(children[0]) == 2
|
||||
assert len(children[1]) == 3
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [2, 2, 1]
|
||||
assert 0 < get_gauge_state("narrow_metric_0").config.width < 190
|
||||
|
||||
|
||||
def test_grid_auto_columns_uses_available_width(dpg_context: None) -> None:
|
||||
with dpg.window(label="Auto", width=640), dpgg.gauge_grid(
|
||||
tag="auto_grid", columns="auto", min_column_width=180
|
||||
):
|
||||
for index in range(7):
|
||||
dpgg.digital_gauge(tag=f"auto_metric_{index}", value=index, height=60)
|
||||
|
||||
children = _children("auto_grid")
|
||||
assert len(children[0]) == 3
|
||||
assert len(children[1]) == 3
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [3, 3, 1]
|
||||
|
||||
|
||||
def test_grid_routes_panels_without_stealing_panel_children(dpg_context: None) -> None:
|
||||
with dpg.window(label="Nested", width=520), dpgg.gauge_grid(tag="nested_grid", columns=2):
|
||||
with dpgg.gauge_panel(tag="nested_panel", height=120):
|
||||
dpgg.digital_gauge(tag="inside_panel", value=1, height=60)
|
||||
dpgg.digital_gauge(tag="beside_panel", value=2, height=60)
|
||||
|
||||
panel_cell = cast(int | str, dpg.get_item_parent("nested_panel"))
|
||||
assert dpg.get_item_type(panel_cell) == "mvAppItemType::mvTableCell"
|
||||
assert dpg.get_item_parent("inside_panel") == "nested_panel"
|
||||
beside_cell = cast(int | str, dpg.get_item_parent("beside_panel"))
|
||||
assert dpg.get_item_type(beside_cell) == "mvAppItemType::mvTableCell"
|
||||
|
||||
|
||||
def test_grid_accepts_gauges_added_after_context(dpg_context: None) -> None:
|
||||
with dpg.window(label="Late", width=520), dpgg.gauge_grid(tag="late_grid", columns=2):
|
||||
dpgg.digital_gauge(tag="late_initial", value=1, height=60)
|
||||
|
||||
dpgg.digital_gauge(tag="late_second", parent="late_grid", value=2, height=60)
|
||||
dpgg.digital_gauge(tag="late_third", parent="late_grid", value=3, height=60)
|
||||
|
||||
children = _children("late_grid")
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [2, 1]
|
||||
|
||||
for tag in ("late_initial", "late_second", "late_third"):
|
||||
cell = cast(int | str, dpg.get_item_parent(tag))
|
||||
assert dpg.get_item_type(cell) == "mvAppItemType::mvTableCell"
|
||||
|
||||
|
||||
def test_auto_grid_in_autosize_panel_wraps_late_gauges(dpg_context: None) -> None:
|
||||
with (
|
||||
dpg.window(label="Autosize", width=1200),
|
||||
dpgg.gauge_panel(
|
||||
tag="autosize_panel", width=1000, height=600, autosize_x=True, autosize_y=True
|
||||
),
|
||||
):
|
||||
dpgg.gauge_grid(tag="autosize_grid", columns="auto", min_column_width=180)
|
||||
|
||||
for index in range(7):
|
||||
dpgg.digital_gauge(
|
||||
tag=f"autosize_metric_{index}", parent="autosize_grid", value=index, height=60
|
||||
)
|
||||
|
||||
children = _children("autosize_grid")
|
||||
assert len(children[0]) == 5
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [5, 2]
|
||||
assert 0 < get_gauge_state("autosize_metric_0").config.width < 200
|
||||
|
||||
|
||||
def test_auto_grid_refreshes_when_autosize_width_becomes_known(dpg_context: None) -> None:
|
||||
with (
|
||||
dpg.window(label="Autosize"),
|
||||
dpgg.gauge_panel(tag="late_width_panel", autosize_x=True, autosize_y=True),
|
||||
):
|
||||
grid = dpgg.gauge_grid(tag="late_width_grid", columns="auto", min_column_width=180)
|
||||
|
||||
assert grid.columns == 1
|
||||
|
||||
dpg.configure_item("late_width_panel", width=1000)
|
||||
for index in range(6):
|
||||
dpgg.digital_gauge(tag=f"late_width_metric_{index}", parent="late_width_grid", value=index)
|
||||
|
||||
children = _children("late_width_grid")
|
||||
assert len(children[0]) == 5
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [5, 1]
|
||||
|
||||
|
||||
def test_auto_grid_reflows_existing_gauges_when_panel_narrows(dpg_context: None) -> None:
|
||||
with dpg.window(label="Resize"), dpgg.gauge_panel(
|
||||
tag="resize_panel", width=1000, height=300
|
||||
):
|
||||
grid = dpgg.gauge_grid(tag="resize_grid", columns="auto", min_column_width=180)
|
||||
with grid:
|
||||
for index in range(6):
|
||||
dpgg.digital_gauge(tag=f"resize_metric_{index}", value=index, height=60)
|
||||
|
||||
assert grid.columns == 5
|
||||
children = _children("resize_grid")
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [5, 1]
|
||||
|
||||
dpg.configure_item("resize_panel", width=430)
|
||||
grid.refresh_columns()
|
||||
|
||||
assert grid.columns == 2
|
||||
children = _children("resize_grid")
|
||||
assert len(children[0]) == 2
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [2, 2, 2]
|
||||
for index in range(6):
|
||||
cell = cast(int | str, dpg.get_item_parent(f"resize_metric_{index}"))
|
||||
assert dpg.get_item_type(cell) == "mvAppItemType::mvTableCell"
|
||||
|
||||
|
||||
def test_grid_compacts_after_runtime_gauge_delete(dpg_context: None) -> None:
|
||||
from dpg_gauges.widget import _cleanup_grid_now
|
||||
|
||||
with dpg.window(label="Reconnect", width=800):
|
||||
dpgg.gauge_grid(tag="connector_grid", columns=3, min_column_width=1)
|
||||
|
||||
for cycle in range(3):
|
||||
tags = [f"connector_{cycle}_{index}" for index in range(3)]
|
||||
for index, tag in enumerate(tags):
|
||||
dpgg.digital_gauge(tag=tag, parent="connector_grid", value=index, height=60)
|
||||
|
||||
children = _children("connector_grid")
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [3]
|
||||
assert _occupied_cell_counts("connector_grid") == [3]
|
||||
|
||||
for tag in tags:
|
||||
dpgg.delete_gauge(tag)
|
||||
|
||||
_cleanup_grid_now("connector_grid")
|
||||
children = _children("connector_grid")
|
||||
assert children[1] == []
|
||||
|
||||
|
||||
def test_grid_reuses_deleted_cell_before_appending(dpg_context: None) -> None:
|
||||
with dpg.window(label="Partial", width=800):
|
||||
dpgg.gauge_grid(tag="partial_grid", columns=3, min_column_width=1)
|
||||
|
||||
for index in range(3):
|
||||
dpgg.digital_gauge(tag=f"partial_{index}", parent="partial_grid", value=index, height=60)
|
||||
|
||||
dpgg.delete_gauge("partial_1")
|
||||
|
||||
assert _occupied_cell_counts("partial_grid") == [2]
|
||||
|
||||
dpgg.digital_gauge(tag="partial_new", parent="partial_grid", value=99, height=60)
|
||||
|
||||
children = _children("partial_grid")
|
||||
assert [len(_children(row)[1]) for row in children[1]] == [3]
|
||||
assert _occupied_cell_counts("partial_grid") == [3]
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# pyright: reportGeneralTypeIssues=false
|
||||
|
||||
from collections.abc import Generator
|
||||
from importlib.resources import files
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
import pytest
|
||||
@@ -30,11 +31,22 @@ def dpg_context() -> Generator[None]:
|
||||
|
||||
|
||||
def test_public_exports_and_version() -> None:
|
||||
assert dpgg.__version__ == "1.0.0"
|
||||
assert dpgg.__version__ == "1.1.2"
|
||||
assert files("dpg_gauges").joinpath("py.typed").is_file()
|
||||
for name in dpgg.__all__:
|
||||
assert hasattr(dpgg, name)
|
||||
|
||||
|
||||
def test_gauge_creation_supports_explicit_parent(dpg_context: None) -> None:
|
||||
dpg.add_window(tag="late_parent", label="Late Parent")
|
||||
|
||||
dpgg.analog_gauge(tag="late_gauge", parent="late_parent", width=180, height=180)
|
||||
|
||||
state = get_gauge_state("late_gauge")
|
||||
assert state.config.parent == "late_parent"
|
||||
assert dpg.get_item_parent(state.container_tag) == "late_parent"
|
||||
|
||||
|
||||
def test_unknown_duplicate_and_deleted_gauge_errors() -> None:
|
||||
with pytest.raises(GaugeNotFoundError):
|
||||
dpgg.set_value("missing", 1)
|
||||
|
||||
Reference in New Issue
Block a user