step 3: add widget shell and renderer foundation

This commit is contained in:
2026-07-02 14:20:22 +02:00
parent 34fd60c34e
commit 41e44a23bc
13 changed files with 683 additions and 84 deletions

View File

@@ -0,0 +1,227 @@
"""Dear PyGui widget shell creation helpers."""
from __future__ import annotations
from dataclasses import dataclass
from types import TracebackType
from typing import Any, Generic, TypeVar, cast
import dearpygui.dearpygui as dpg
from .gauges import (
AnalogGaugeConfig,
BarGaugeConfig,
BatteryGaugeConfig,
CompassGaugeConfig,
DigitalGaugeConfig,
GaugeConfig,
LevelGaugeConfig,
MultiValueGaugeConfig,
SegmentedGaugeConfig,
StatusLightConfig,
ThermometerGaugeConfig,
)
from .renderer import GaugeRenderer
from .state import GaugeState, register_gauge
from .types import Tag
ConfigT = TypeVar("ConfigT", bound=GaugeConfig)
_dpg_context_active = False
_original_create_context = dpg.create_context
_original_destroy_context = dpg.destroy_context
def _install_context_tracking() -> None:
if getattr(dpg.create_context, "__dpg_gauges_wrapped__", False):
return
def create_context(*args: Any, **kwargs: Any) -> Any:
global _dpg_context_active
result = _original_create_context(*args, **kwargs)
_dpg_context_active = True
return result
def destroy_context(*args: Any, **kwargs: Any) -> Any:
global _dpg_context_active
try:
return _original_destroy_context(*args, **kwargs)
finally:
_dpg_context_active = False
create_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
destroy_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined]
dpg.create_context = create_context
dpg.destroy_context = destroy_context
_install_context_tracking()
@dataclass
class GaugeContext(Generic[ConfigT]):
state: GaugeState
def __enter__(self) -> ConfigT:
if _dpg_context_active and dpg.does_item_exist(self.state.container_tag):
dpg.push_container_stack(self.state.container_tag)
return cast(ConfigT, self.state.config)
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
traceback: TracebackType | None,
) -> None:
if _dpg_context_active and dpg.does_item_exist(self.state.container_tag):
dpg.pop_container_stack()
@dataclass
class LayoutContext:
tag: Tag | None
def __enter__(self) -> Tag | None:
if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag):
dpg.push_container_stack(self.tag)
return self.tag
def __exit__(
self,
exc_type: type[BaseException] | None,
exc: BaseException | None,
traceback: TracebackType | None,
) -> None:
if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag):
dpg.pop_container_stack()
def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[ConfigT]:
if not _dpg_context_active:
return GaugeContext(register_gauge(gauge_type, config))
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
width = config.width
height = config.height or _default_height(gauge_type)
if height == -1:
height = _default_height(gauge_type)
container = dpg.add_group(
tag=container_tag or 0,
width=width,
height=height,
show=config.show,
user_data=config.user_data,
)
drawlist = dpg.add_drawlist(
max(width, 1) if width > 0 else _default_width(gauge_type),
max(height, 1),
tag=drawlist_tag or 0,
parent=container,
show=config.show,
)
_add_tooltip(container, config.tooltip)
state = register_gauge(
gauge_type,
config,
container_tag=container,
drawlist_tag=drawlist,
text_tags={},
)
renderer = GaugeRenderer(state)
state.renderer = renderer
renderer.render()
return GaugeContext(state)
def gauge_panel(**config: Any) -> LayoutContext:
if not _dpg_context_active:
return LayoutContext(None)
tag = config.pop("tag", 0)
label = config.pop("label", None)
tooltip = config.pop("tooltip", None)
container = dpg.add_child_window(
tag=tag,
label=label,
width=config.pop("width", 0),
height=config.pop("height", 0),
show=config.pop("show", True),
border=config.pop("border", True),
autosize_x=config.pop("autosize_x", False),
autosize_y=config.pop("autosize_y", False),
**config,
)
_add_tooltip(container, tooltip)
return LayoutContext(container)
def gauge_grid(**config: Any) -> LayoutContext:
if not _dpg_context_active:
return LayoutContext(None)
columns = max(int(config.pop("columns", 1)), 1)
container = dpg.add_group(
tag=config.pop("tag", 0),
width=config.pop("width", 0),
height=config.pop("height", 0),
show=config.pop("show", True),
horizontal=columns > 1,
**config,
)
return LayoutContext(container)
def analog_gauge(**config: Any) -> GaugeContext[AnalogGaugeConfig]:
return create_gauge_context("analog", AnalogGaugeConfig(**config))
def digital_gauge(**config: Any) -> GaugeContext[DigitalGaugeConfig]:
return create_gauge_context("digital", DigitalGaugeConfig(**config))
def bar_gauge(**config: Any) -> GaugeContext[BarGaugeConfig]:
return create_gauge_context("bar", BarGaugeConfig(**config))
def level_gauge(**config: Any) -> GaugeContext[LevelGaugeConfig]:
return create_gauge_context("level", LevelGaugeConfig(**config))
def status_light(**config: Any) -> GaugeContext[StatusLightConfig]:
return create_gauge_context("status_light", StatusLightConfig(**config))
def segmented_gauge(**config: Any) -> GaugeContext[SegmentedGaugeConfig]:
return create_gauge_context("segmented", SegmentedGaugeConfig(**config))
def compass_gauge(**config: Any) -> GaugeContext[CompassGaugeConfig]:
return create_gauge_context("compass", CompassGaugeConfig(**config))
def thermometer_gauge(**config: Any) -> GaugeContext[ThermometerGaugeConfig]:
return create_gauge_context("thermometer", ThermometerGaugeConfig(**config))
def battery_gauge(**config: Any) -> GaugeContext[BatteryGaugeConfig]:
return create_gauge_context("battery", BatteryGaugeConfig(**config))
def multi_value_gauge(**config: Any) -> GaugeContext[MultiValueGaugeConfig]:
return create_gauge_context("multi_value", MultiValueGaugeConfig(**config))
def _default_width(gauge_type: str) -> int:
return 220 if gauge_type in {"analog", "compass"} else 180
def _default_height(gauge_type: str) -> int:
return 220 if gauge_type in {"analog", "compass"} else 80
def _add_tooltip(parent: Tag, tooltip: str | None) -> None:
if not tooltip:
return
tooltip_tag = dpg.add_tooltip(parent)
dpg.add_text(tooltip, parent=tooltip_tag)