Release 1.1.1

This commit is contained in:
2026-07-08 11:06:17 +01:00
parent 0a7b4f0172
commit 5b1826d9c3
7 changed files with 382 additions and 32 deletions

View File

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

View File

@@ -43,7 +43,9 @@ _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] = {}
_layout_suspend_depth = 0
_grid_refresh_scheduled = False
def _install_context_tracking() -> None:
@@ -57,11 +59,14 @@ 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()
_grid_refresh_scheduled = False
create_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
destroy_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
@@ -131,6 +136,9 @@ class LayoutContext:
@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
@@ -139,6 +147,7 @@ class GridLayoutContext:
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:
@@ -161,10 +170,13 @@ class GridLayoutContext:
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.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:
@@ -172,15 +184,35 @@ class GridLayoutContext:
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 = _next_layout_cell(config.parent)
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 _current_grid_fits_gauges() and config.width <= 0:
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)
@@ -223,11 +255,21 @@ 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]:
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 parent is not None or grid is None:
return None, None
return grid.next_cell()
if grid is None:
return None, None, None
cell, width = grid.next_cell()
return cell, width, grid
def _current_grid() -> GridLayoutContext | None:
@@ -239,25 +281,147 @@ def _current_grid() -> GridLayoutContext | 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)
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 max(dpg.get_item_width(container) or 0, 0)
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:
@@ -289,7 +453,7 @@ def gauge_panel(
"""Create a lightweight child-window container for related gauges."""
if not _dpg_context_active:
return LayoutContext(None)
layout_parent, layout_width = _next_layout_cell(parent)
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:
@@ -326,14 +490,24 @@ def gauge_grid(
) -> GridLayoutContext:
"""Create a lightweight wrapping gauge layout container."""
if not _dpg_context_active:
return GridLayoutContext(None, 1, min_column_width, fit_gauges, cell_padding)
layout_parent, layout_width = _next_layout_cell(parent)
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)
columns = _resolved_columns(columns, min_column_width, available_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,
@@ -345,17 +519,24 @@ def gauge_grid(
scrollX=False,
**config,
)
for _ in range(columns):
for _ in range(resolved_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,
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(