3 Commits

Author SHA1 Message Date
accd1d1dee Merge dev for 1.0.2 release 2026-07-07 16:42:34 +01:00
fe6670af9e Merge dev for 1.0.1 release 2026-07-07 17:17:55 +02:00
63e79ebf8d Merge dev for 1.0.0 release 2026-07-07 16:44:20 +02:00
11 changed files with 16 additions and 651 deletions

View File

@@ -37,18 +37,12 @@ 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, min_column_width=180): with dpgg.gauge_grid(columns=3):
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 Gauges also accept Dear PyGui's usual `parent` argument for adding a gauge to an
existing container after that container has already been created: existing container after that container has already been created:

View File

@@ -24,14 +24,7 @@ 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(columns=1, min_column_width=180, fit_gauges=True, cell_padding=12, **config)`: - `gauge_grid(**config)`: lightweight horizontal gauge group.
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,7 +15,6 @@ 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`

View File

@@ -1,91 +0,0 @@
# 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()

View File

@@ -1,65 +0,0 @@
# 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.1.1" version = "1.0.2"
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.1.1" __version__ = "1.0.2"
__all__ = [ __all__ = [
"configure", "configure",

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, replace from dataclasses import dataclass
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,10 +42,6 @@ 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] = []
_grid_layouts: dict[Tag, GridLayoutContext] = {}
_layout_suspend_depth = 0
_grid_refresh_scheduled = False
def _install_context_tracking() -> None: def _install_context_tracking() -> None:
@@ -59,14 +55,11 @@ def _install_context_tracking() -> None:
return result return result
def destroy_context(*args: Any, **kwargs: Any) -> Any: def destroy_context(*args: Any, **kwargs: Any) -> Any:
global _dpg_context_active, _grid_refresh_scheduled global _dpg_context_active
try: try:
return _original_destroy_context(*args, **kwargs) return _original_destroy_context(*args, **kwargs)
finally: finally:
_dpg_context_active = False _dpg_context_active = False
_active_grids.clear()
_grid_layouts.clear()
_grid_refresh_scheduled = False
create_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined] create_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
destroy_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined] destroy_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
@@ -80,14 +73,10 @@ _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__(
@@ -96,27 +85,17 @@ 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__(
@@ -125,97 +104,14 @@ 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
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()
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]: 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, 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 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
@@ -255,187 +151,6 @@ 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, 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:
_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 {})
cells.append(list(cell_children.get(1, [])))
return cells
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 _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]
continue
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( def gauge_panel(
*, *,
tag: Tag | None = None, tag: Tag | None = None,
@@ -453,11 +168,6 @@ 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, _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( container = dpg.add_child_window(
tag=tag or 0, tag=tag or 0,
parent=parent or 0, parent=parent or 0,
@@ -476,67 +186,28 @@ def gauge_panel(
def gauge_grid( def gauge_grid(
*, *,
columns: int | Literal["auto"] = 1, columns: int = 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, 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,
) -> GridLayoutContext: ) -> LayoutContext:
"""Create a lightweight wrapping gauge layout container.""" """Create a lightweight horizontal gauge layout container."""
if not _dpg_context_active: if not _dpg_context_active:
return GridLayoutContext( return LayoutContext(None)
tag=None, columns = max(int(columns), 1)
parent=None, container = dpg.add_group(
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, tag=tag or 0,
parent=parent or 0, parent=parent or 0,
width=width, width=width,
height=height, height=height,
show=show, show=show,
header_row=False, horizontal=columns > 1,
policy=dpg.mvTable_SizingStretchSame,
scrollX=False,
**config, **config,
) )
for _ in range(resolved_columns): return LayoutContext(container)
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( def analog_gauge(

View File

@@ -1,7 +1,6 @@
# 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
@@ -10,10 +9,6 @@ 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()
@@ -81,134 +76,3 @@ 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"
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"

View File

@@ -31,7 +31,7 @@ def dpg_context() -> Generator[None]:
def test_public_exports_and_version() -> None: def test_public_exports_and_version() -> None:
assert dpgg.__version__ == "1.1.1" assert dpgg.__version__ == "1.0.2"
assert files("dpg_gauges").joinpath("py.typed").is_file() 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)

2
uv.lock generated
View File

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