Improve gauge grid layout
This commit is contained in:
@@ -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,8 @@ 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] = []
|
||||
_layout_suspend_depth = 0
|
||||
|
||||
|
||||
def _install_context_tracking() -> None:
|
||||
@@ -73,10 +75,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 +91,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 +120,69 @@ 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
|
||||
columns: int
|
||||
min_column_width: int
|
||||
fit_gauges: bool
|
||||
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), 1)
|
||||
|
||||
|
||||
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)
|
||||
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
|
||||
container_tag = tag if tag is not None else None
|
||||
drawlist_tag = f"{tag}__drawlist" if tag is not None else None
|
||||
@@ -151,6 +222,49 @@ 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]:
|
||||
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, min_column_width: int, available_width: int) -> int:
|
||||
columns = max(int(columns), 1)
|
||||
min_column_width = max(int(min_column_width), 0)
|
||||
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,
|
||||
@@ -168,6 +282,11 @@ 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)
|
||||
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,
|
||||
@@ -187,27 +306,42 @@ def gauge_panel(
|
||||
def gauge_grid(
|
||||
*,
|
||||
columns: int = 1,
|
||||
min_column_width: int = 180,
|
||||
fit_gauges: bool = True,
|
||||
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(None, 1, min_column_width, fit_gauges)
|
||||
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
|
||||
available_width = _available_layout_width(parent, width)
|
||||
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(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, row_height, available_width
|
||||
)
|
||||
|
||||
|
||||
def analog_gauge(
|
||||
|
||||
Reference in New Issue
Block a user