9 Commits

14 changed files with 379 additions and 32 deletions

View File

@@ -37,12 +37,29 @@ dpg.destroy_context()
```python ```python
with dpg.window(label="Engine"): with dpg.window(label="Engine"):
with dpgg.gauge_panel(label="Powertrain", width=-1, height=260): 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.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.bar_gauge(tag="boost", label="Boost", max_value=30, unit="psi")
dpgg.status_light(tag="ecu", label="ECU", state=True) 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 ## GUI-Thread Contract
Dear PyGui item operations must happen on the GUI thread. Gauge creation and runtime updates such as Dear PyGui item operations must happen on the GUI thread. Gauge creation and runtime updates such as

View File

@@ -24,7 +24,14 @@ Creation functions are context managers and accept common options such as `tag`,
## Layout Helpers ## Layout Helpers
- `gauge_panel(**config)`: child-window panel for grouped gauges. - `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 ## Runtime API

View File

@@ -15,6 +15,7 @@ Run examples with `uv run python examples/<name>.py`.
- `thresholds_markers.py` - `thresholds_markers.py`
- `animations.py` - `animations.py`
- `dashboard.py` - `dashboard.py`
- `layout_grid.py`
- `sizing.py` - `sizing.py`
- `live_high_rate.py` - `live_high_rate.py`
- `live_worst_case.py` - `live_worst_case.py`

65
examples/layout_grid.py Normal file
View 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()

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "dpg-gauges" name = "dpg-gauges"
version = "1.0.0" version = "1.1.0"
description = "Dear PyGui gauge widgets for dashboards and telemetry displays" description = "Dear PyGui gauge widgets for dashboards and telemetry displays"
readme = "README.md" readme = "README.md"
authors = [ authors = [

View File

@@ -35,7 +35,7 @@ from .types import (
ThresholdBand, ThresholdBand,
) )
__version__ = "1.0.0" __version__ = "1.1.0"
__all__ = [ __all__ = [
"configure", "configure",

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
from typing import Any from typing import Any
from . import widget as _widget
from .gauges import GaugeConfig from .gauges import GaugeConfig
from .state import ( from .state import (
configure_gauges, configure_gauges,
@@ -17,6 +16,44 @@ from .state import (
update_runtime_gauge, update_runtime_gauge,
) )
from .types import GaugeMarker, GaugeValue, Tag, ThresholdBand 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: def configure(**config: Any) -> None:
@@ -24,20 +61,6 @@ def configure(**config: Any) -> None:
configure_gauges(**config) 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: def set_value(tag: Tag, value: GaugeValue) -> None:
"""Set a gauge target value from the Dear PyGui GUI thread.""" """Set a gauge target value from the Dear PyGui GUI thread."""
set_runtime_value(tag, value) set_runtime_value(tag, value)
@@ -78,4 +101,4 @@ def delete_gauge(tag: Tag) -> None:
delete_runtime_gauge(tag) delete_runtime_gauge(tag)
__all_configs__ = (GaugeConfig,) __all_configs__: tuple[type[GaugeConfig], ...] = (GaugeConfig,)

View File

@@ -36,6 +36,7 @@ def _normalize_smoothing(value: SmoothingConfig | bool | None) -> SmoothingConfi
@dataclass(frozen=True) @dataclass(frozen=True)
class GaugeConfig: class GaugeConfig:
tag: Tag | None = None tag: Tag | None = None
parent: Tag | None = None
label: str | None = None label: str | None = None
value: GaugeValue = None value: GaugeValue = None
min_value: float = 0.0 min_value: float = 0.0

0
src/dpg_gauges/py.typed Normal file
View File

View File

@@ -50,7 +50,7 @@ class GaugeRenderer:
def __init__(self, state: GaugeState) -> None: def __init__(self, state: GaugeState) -> None:
self.state = state self.state = state
self.layers = DrawLayers() self.layers: DrawLayers = DrawLayers()
self._scheduled = False self._scheduled = False
def render(self) -> None: def render(self) -> None:

View File

@@ -3,7 +3,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping, Sequence from collections.abc import Mapping, Sequence
from dataclasses import dataclass from dataclasses import dataclass, replace
from types import TracebackType from types import TracebackType
from typing import Any, Generic, Literal, TypeVar, cast from typing import Any, Generic, Literal, TypeVar, cast
@@ -42,6 +42,8 @@ ConfigT = TypeVar("ConfigT", bound=GaugeConfig)
_dpg_context_active = False _dpg_context_active = False
_original_create_context = dpg.create_context _original_create_context = dpg.create_context
_original_destroy_context = dpg.destroy_context _original_destroy_context = dpg.destroy_context
_active_grids: list[GridLayoutContext] = []
_layout_suspend_depth = 0
def _install_context_tracking() -> None: def _install_context_tracking() -> None:
@@ -73,10 +75,14 @@ _install_context_tracking()
@dataclass @dataclass
class GaugeContext(Generic[ConfigT]): class GaugeContext(Generic[ConfigT]):
state: GaugeState state: GaugeState
_suspended_layout: bool = False
def __enter__(self) -> ConfigT: def __enter__(self) -> ConfigT:
global _layout_suspend_depth
if _dpg_context_active and dpg.does_item_exist(self.state.container_tag): if _dpg_context_active and dpg.does_item_exist(self.state.container_tag):
dpg.push_container_stack(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) return cast(ConfigT, self.state.config)
def __exit__( def __exit__(
@@ -85,17 +91,27 @@ class GaugeContext(Generic[ConfigT]):
exc: BaseException | None, exc: BaseException | None,
traceback: TracebackType | None, traceback: TracebackType | None,
) -> None: ) -> None:
global _layout_suspend_depth
if _dpg_context_active and dpg.does_item_exist(self.state.container_tag): if _dpg_context_active and dpg.does_item_exist(self.state.container_tag):
dpg.pop_container_stack() dpg.pop_container_stack()
if self._suspended_layout:
_layout_suspend_depth = max(_layout_suspend_depth - 1, 0)
self._suspended_layout = False
@dataclass @dataclass
class LayoutContext: class LayoutContext:
tag: Tag | None tag: Tag | None
suspend_layout: bool = True
_suspended_layout: bool = False
def __enter__(self) -> Tag | None: 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): if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag):
dpg.push_container_stack(self.tag) dpg.push_container_stack(self.tag)
if self.suspend_layout:
_layout_suspend_depth += 1
self._suspended_layout = True
return self.tag return self.tag
def __exit__( def __exit__(
@@ -104,14 +120,70 @@ class LayoutContext:
exc: BaseException | None, exc: BaseException | None,
traceback: TracebackType | None, traceback: TracebackType | None,
) -> None: ) -> None:
global _layout_suspend_depth
if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag): if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag):
dpg.pop_container_stack() 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
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
_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
if self._next_index % self.columns == 0:
self._current_row = dpg.add_table_row(parent=self.tag, height=self.row_height)
cell = dpg.add_table_cell(parent=self._current_row or self.tag)
self._next_index += 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 create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[ConfigT]: def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[ConfigT]:
if not _dpg_context_active: if not _dpg_context_active:
return GaugeContext(register_gauge(gauge_type, config)) return GaugeContext(register_gauge(gauge_type, config))
layout_parent, layout_width = _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 _current_grid_fits_gauges() and config.width <= 0:
updates["width"] = layout_width
config = replace(config, **updates)
tag = config.tag tag = config.tag
container_tag = tag if tag is not None else None container_tag = tag if tag is not None else None
drawlist_tag = f"{tag}__drawlist" if tag is not None else None drawlist_tag = f"{tag}__drawlist" if tag is not None else None
@@ -122,6 +194,7 @@ def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[Confi
container = dpg.add_group( container = dpg.add_group(
tag=container_tag or 0, tag=container_tag or 0,
parent=config.parent or 0,
width=width, width=width,
height=height, height=height,
show=config.show, show=config.show,
@@ -150,9 +223,59 @@ def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[Confi
return GaugeContext(state) return GaugeContext(state)
def _next_layout_cell(parent: Tag | None) -> tuple[Tag | None, int | None]:
grid = _current_grid()
if parent is not None or grid is None:
return None, None
return grid.next_cell()
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 _current_grid_fits_gauges() -> bool:
grid = _current_grid()
return grid.fit_gauges if grid is not None else False
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 max(dpg.get_item_width(parent) or 0, 0)
try:
container = dpg.top_container_stack()
except Exception:
return 0
if container is not None and dpg.does_item_exist(container):
return max(dpg.get_item_width(container) or 0, 0)
return 0
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( def gauge_panel(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
width: int = 0, width: int = 0,
height: int = 0, height: int = 0,
@@ -166,8 +289,14 @@ def gauge_panel(
"""Create a lightweight child-window container for related gauges.""" """Create a lightweight child-window container for related gauges."""
if not _dpg_context_active: if not _dpg_context_active:
return LayoutContext(None) return LayoutContext(None)
layout_parent, layout_width = _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( container = dpg.add_child_window(
tag=tag or 0, tag=tag or 0,
parent=parent or 0,
label=label or "", label=label or "",
width=width, width=width,
height=height, height=height,
@@ -183,31 +312,56 @@ def gauge_panel(
def gauge_grid( 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, tag: Tag | None = None,
parent: Tag | None = None,
width: int = 0, width: int = 0,
height: int = 0, height: int = 0,
show: bool = True, show: bool = True,
**config: Any, **config: Any,
) -> LayoutContext: ) -> GridLayoutContext:
"""Create a lightweight horizontal gauge layout container.""" """Create a lightweight wrapping gauge layout container."""
if not _dpg_context_active: if not _dpg_context_active:
return LayoutContext(None) return GridLayoutContext(None, 1, min_column_width, fit_gauges, cell_padding)
columns = max(int(columns), 1) layout_parent, layout_width = _next_layout_cell(parent)
container = dpg.add_group( 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)
columns = _resolved_columns(columns, min_column_width, available_width)
container = dpg.add_table(
tag=tag or 0, tag=tag or 0,
parent=parent or 0,
width=width, width=width,
height=height, height=height,
show=show, show=show,
horizontal=columns > 1, header_row=False,
policy=dpg.mvTable_SizingStretchSame,
scrollX=False,
**config, **config,
) )
return LayoutContext(container) for _ in range(columns):
dpg.add_table_column(parent=container, width_stretch=True, init_width_or_weight=1.0)
return GridLayoutContext(
container,
columns,
min_column_width,
fit_gauges,
cell_padding,
row_height,
available_width,
)
def analog_gauge( def analog_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -253,6 +407,7 @@ def analog_gauge(
def digital_gauge( def digital_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -287,6 +442,7 @@ def digital_gauge(
def bar_gauge( def bar_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -326,6 +482,7 @@ def bar_gauge(
def level_gauge( def level_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -365,6 +522,7 @@ def level_gauge(
def status_light( def status_light(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -403,6 +561,7 @@ def status_light(
def segmented_gauge( def segmented_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -443,6 +602,7 @@ def segmented_gauge(
def compass_gauge( def compass_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -488,6 +648,7 @@ def compass_gauge(
def thermometer_gauge( def thermometer_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -529,6 +690,7 @@ def thermometer_gauge(
def battery_gauge( def battery_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,
@@ -569,6 +731,7 @@ def battery_gauge(
def multi_value_gauge( def multi_value_gauge(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
parent: Tag | None = None,
label: str | None = None, label: str | None = None,
value: GaugeValue = None, value: GaugeValue = None,
min_value: float = 0.0, min_value: float = 0.0,

View File

@@ -1,6 +1,7 @@
# pyright: reportGeneralTypeIssues=false # pyright: reportGeneralTypeIssues=false
from collections.abc import Generator from collections.abc import Generator
from typing import cast
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
import pytest import pytest
@@ -9,6 +10,10 @@ import dpg_gauges as dpgg
from dpg_gauges.state import clear_registry, get_gauge_state 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))
@pytest.fixture @pytest.fixture
def dpg_context() -> Generator[None]: def dpg_context() -> Generator[None]:
clear_registry() clear_registry()
@@ -76,3 +81,56 @@ def test_panel_and_grid_are_lightweight_containers(dpg_context: None) -> None:
assert dpg.does_item_exist("engine") assert dpg.does_item_exist("engine")
assert dpg.does_item_exist("engine_grid") assert dpg.does_item_exist("engine_grid")
assert get_gauge_state("temp").renderer is not None 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"

View File

@@ -1,6 +1,7 @@
# pyright: reportGeneralTypeIssues=false # pyright: reportGeneralTypeIssues=false
from collections.abc import Generator from collections.abc import Generator
from importlib.resources import files
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
import pytest import pytest
@@ -30,11 +31,22 @@ def dpg_context() -> Generator[None]:
def test_public_exports_and_version() -> None: def test_public_exports_and_version() -> None:
assert dpgg.__version__ == "1.0.0" assert dpgg.__version__ == "1.1.0"
assert files("dpg_gauges").joinpath("py.typed").is_file()
for name in dpgg.__all__: for name in dpgg.__all__:
assert hasattr(dpgg, name) 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: def test_unknown_duplicate_and_deleted_gauge_errors() -> None:
with pytest.raises(GaugeNotFoundError): with pytest.raises(GaugeNotFoundError):
dpgg.set_value("missing", 1) dpgg.set_value("missing", 1)

2
uv.lock generated
View File

@@ -36,7 +36,7 @@ wheels = [
[[package]] [[package]]
name = "dpg-gauges" name = "dpg-gauges"
version = "1.0.0" version = "1.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "dearpygui" }, { name = "dearpygui" },