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

@@ -2,8 +2,9 @@
Dear PyGui gauge widgets for dashboards and telemetry displays.
Current implementation status: Step 2 logical state is implemented. Gauge creation functions build
and register pure gauge state without requiring a Dear PyGui context; final widget shells and drawing
arrive in later steps.
Current implementation status: Step 3 widget shells are implemented. Gauge creation functions still
register pure gauge state without requiring a Dear PyGui context, and when a Dear PyGui context is
active they create a group plus drawlist shell with placeholder frame and value text rendering.
Runtime value updates are intended for the Dear PyGui GUI thread once rendering is implemented.
Runtime value updates are intended for the Dear PyGui GUI thread. Attached gauges redraw immediately
for simple value/configuration changes.

View File

@@ -2,7 +2,7 @@
## Current status
Step 2 completed and verified. Ready to start Step 3.
Step 3 completed and verified with automated checks. Ready to start Step 4.
## Completed steps
@@ -28,7 +28,18 @@ Step 1 - Public API contract and pure core:
## Current step
Step 3 - Widget shell and renderer foundation.
Step 4 - Digital, bar, level, and status gauges.
Step 3 - Widget shell and renderer foundation:
Implemented during Step 3:
- Added Dear PyGui widget shell creation for gauge context managers when a DPG context is active.
- Kept pure registration behavior when no DPG context has been created.
- Added a renderer foundation that measures drawlist size, redraws stable placeholder background/frame/value text, schedules animation frames, and exposes renderer debug fields.
- Added draw layer bookkeeping.
- Added lightweight `gauge_panel` and `gauge_grid` containers.
- Added Step 3 examples and smoke tests.
Step 2 - State, sizing, animation, and smoothing model:
@@ -58,7 +69,7 @@ Implemented during Step 2:
## Known issues
None yet.
Manual visual example checks were not run in this headless session.
## Commands used
@@ -84,7 +95,14 @@ None yet.
- Ran `uv run ruff format --check .`.
- Ran `uv run pyright`.
- Updated instructions to require a commit after each completed step.
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 3 changes.
- Implemented Step 3 widget shells, renderer foundation, draw layer bookkeeping, layout helpers, examples, smoke tests, README status update, and diagnostics renderer fields.
- Ran `uv run pytest`.
- Ran `uv run ruff check .`.
- Ran `uv run ruff format .`.
- Ran `uv run ruff format --check .`.
- Ran `uv run pyright`.
## Next action
Implement Step 3.
Implement Step 4.

25
examples/basic_analog.py Normal file
View File

@@ -0,0 +1,25 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Analog Gauge", width=320, height=320):
dpgg.analog_gauge(
tag="rpm", label="RPM", value=3250, max_value=8000, width=240, height=240
)
dpg.create_viewport(title="dpg-gauges analog", width=360, height=360)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

25
examples/basic_digital.py Normal file
View File

@@ -0,0 +1,25 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Digital Gauge", width=320, height=180):
dpgg.digital_gauge(
tag="speed", label="Speed", value=72, unit="km/h", width=260, height=100
)
dpg.create_viewport(title="dpg-gauges digital", width=360, height=220)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

35
examples/dashboard.py Normal file
View File

@@ -0,0 +1,35 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with (
dpg.window(label="Dashboard", width=620, height=360),
dpgg.gauge_panel(label="Engine", width=580, height=260),
dpgg.gauge_grid(columns=3),
):
dpgg.analog_gauge(
tag="rpm", label="RPM", value=4200, max_value=8000, width=180, height=180
)
dpgg.digital_gauge(
tag="speed", label="Speed", value=88, unit="km/h", width=160, height=90
)
dpgg.digital_gauge(
tag="temp", label="Coolant", value=91, unit="C", width=160, height=90
)
dpg.create_viewport(title="dpg-gauges dashboard", width=660, height=400)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

25
examples/sizing.py Normal file
View File

@@ -0,0 +1,25 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Sizing", width=520, height=300):
dpgg.digital_gauge(tag="fixed", label="Fixed", value=12.3, width=180, height=80)
dpgg.digital_gauge(tag="wide", label="Fill width", value=45.6, width=-1, height=80)
dpgg.analog_gauge(tag="square", label="Square", value=60, width=180, height=180)
dpg.create_viewport(title="dpg-gauges sizing", width=560, height=340)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

View File

@@ -2,27 +2,14 @@
from __future__ import annotations
from contextlib import nullcontext
from typing import Any, cast
from typing import Any
from .gauges import (
AnalogGaugeConfig,
BarGaugeConfig,
BatteryGaugeConfig,
CompassGaugeConfig,
DigitalGaugeConfig,
GaugeConfig,
LevelGaugeConfig,
MultiValueGaugeConfig,
SegmentedGaugeConfig,
StatusLightConfig,
ThermometerGaugeConfig,
)
from . import widget as _widget
from .gauges import GaugeConfig
from .state import (
configure_gauges,
delete_runtime_gauge,
get_runtime_value,
register_gauge,
set_runtime_markers,
set_runtime_show,
set_runtime_thresholds,
@@ -36,62 +23,18 @@ def configure(**config: Any) -> None:
configure_gauges(**config)
def analog_gauge(**config: Any) -> nullcontext[AnalogGaugeConfig]:
state = register_gauge("analog", AnalogGaugeConfig(**config))
return nullcontext(cast(AnalogGaugeConfig, state.config))
def digital_gauge(**config: Any) -> nullcontext[DigitalGaugeConfig]:
state = register_gauge("digital", DigitalGaugeConfig(**config))
return nullcontext(cast(DigitalGaugeConfig, state.config))
def bar_gauge(**config: Any) -> nullcontext[BarGaugeConfig]:
state = register_gauge("bar", BarGaugeConfig(**config))
return nullcontext(cast(BarGaugeConfig, state.config))
def level_gauge(**config: Any) -> nullcontext[LevelGaugeConfig]:
state = register_gauge("level", LevelGaugeConfig(**config))
return nullcontext(cast(LevelGaugeConfig, state.config))
def status_light(**config: Any) -> nullcontext[StatusLightConfig]:
state = register_gauge("status_light", StatusLightConfig(**config))
return nullcontext(cast(StatusLightConfig, state.config))
def segmented_gauge(**config: Any) -> nullcontext[SegmentedGaugeConfig]:
state = register_gauge("segmented", SegmentedGaugeConfig(**config))
return nullcontext(cast(SegmentedGaugeConfig, state.config))
def compass_gauge(**config: Any) -> nullcontext[CompassGaugeConfig]:
state = register_gauge("compass", CompassGaugeConfig(**config))
return nullcontext(cast(CompassGaugeConfig, state.config))
def thermometer_gauge(**config: Any) -> nullcontext[ThermometerGaugeConfig]:
state = register_gauge("thermometer", ThermometerGaugeConfig(**config))
return nullcontext(cast(ThermometerGaugeConfig, state.config))
def battery_gauge(**config: Any) -> nullcontext[BatteryGaugeConfig]:
state = register_gauge("battery", BatteryGaugeConfig(**config))
return nullcontext(cast(BatteryGaugeConfig, state.config))
def multi_value_gauge(**config: Any) -> nullcontext[MultiValueGaugeConfig]:
state = register_gauge("multi_value", MultiValueGaugeConfig(**config))
return nullcontext(cast(MultiValueGaugeConfig, state.config))
def gauge_panel(**_config: Any) -> nullcontext[None]:
return nullcontext(None)
def gauge_grid(**_config: Any) -> nullcontext[None]:
return nullcontext(None)
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:

View File

@@ -10,7 +10,7 @@ from .types import Tag
def get_gauge_debug_state(tag: Tag) -> dict[str, Any]:
state = get_gauge_state(tag)
return {
debug = {
"tag": state.tag,
"gauge_type": state.gauge_type,
"target_value": state.target_value,
@@ -26,3 +26,6 @@ def get_gauge_debug_state(tag: Tag) -> dict[str, Any]:
"animation_active": state.animation_state.active,
"smoothing_active": state.smoothing_state.active,
}
if state.renderer is not None:
debug["renderer"] = state.renderer.debug_state()
return debug

View File

@@ -0,0 +1,44 @@
"""Bookkeeping for Dear PyGui drawlist layers."""
from __future__ import annotations
from dataclasses import dataclass, field
from .types import Tag
@dataclass
class DrawLayer:
name: str
item_tags: set[Tag] = field(default_factory=set)
def add(self, tag: Tag) -> Tag:
self.item_tags.add(tag)
return tag
def clear(self) -> None:
self.item_tags.clear()
@dataclass
class DrawLayers:
background: DrawLayer = field(default_factory=lambda: DrawLayer("background"))
static: DrawLayer = field(default_factory=lambda: DrawLayer("static"))
thresholds: DrawLayer = field(default_factory=lambda: DrawLayer("thresholds"))
dynamic: DrawLayer = field(default_factory=lambda: DrawLayer("dynamic"))
markers: DrawLayer = field(default_factory=lambda: DrawLayer("markers"))
text: DrawLayer = field(default_factory=lambda: DrawLayer("text"))
def all_layers(self) -> tuple[DrawLayer, ...]:
return (
self.background,
self.static,
self.thresholds,
self.dynamic,
self.markers,
self.text,
)
def clear(self) -> None:
for layer in self.all_layers():
layer.clear()

View File

@@ -0,0 +1,166 @@
"""Dear PyGui renderer foundation for gauge placeholders."""
from __future__ import annotations
import time
from typing import Any
import dearpygui.dearpygui as dpg
from .animation import advance_animation
from .draw_layers import DrawLayers
from .scales import format_gauge_value
from .sizing import effective_size, update_measured_size
from .smoothing import advance_smoothing
from .state import DirtyFlags, GaugeState, animation_config, size_state_for_gauge, update_state_size
from .themes import GaugeStyle
from .types import SmoothingConfig
class GaugeRenderer:
"""Small renderer that keeps a gauge drawlist populated between updates."""
def __init__(self, state: GaugeState) -> None:
self.state = state
self.layers = DrawLayers()
self._scheduled = False
def render(self) -> None:
state = self.state
self._measure()
self._advance_values()
self._redraw_placeholder()
state.dirty = DirtyFlags.NONE
if state.animation_state.active or state.smoothing_state.active:
self.schedule_frame()
def delete(self) -> None:
if dpg.does_item_exist(self.state.container_tag):
dpg.delete_item(self.state.container_tag)
def schedule_frame(self) -> None:
if self._scheduled:
return
self._scheduled = True
frame = dpg.get_frame_count() + 1
dpg.set_frame_callback(frame, self._frame_callback)
def debug_state(self) -> dict[str, Any]:
state = self.state
return {
"container_tag": state.container_tag,
"drawlist_tag": state.drawlist_tag,
"layers": {layer.name: tuple(layer.item_tags) for layer in self.layers.all_layers()},
"scheduled": self._scheduled,
}
def _frame_callback(self) -> None:
self._scheduled = False
self.state.dirty |= DirtyFlags.DYNAMIC | DirtyFlags.TEXT
self.render()
def _measure(self) -> None:
state = self.state
if state.drawlist_tag is None or not dpg.does_item_exist(state.drawlist_tag):
return
width = dpg.get_item_width(state.drawlist_tag) or 0
height = dpg.get_item_height(state.drawlist_tag) or 0
size = size_state_for_gauge(state)
if update_measured_size(size, width, height):
update_state_size(state, size)
def _advance_values(self) -> None:
state = self.state
now = time.monotonic()
config = animation_config(state.config)
if state.animation_state.active:
state.displayed_value, active = advance_animation(state.animation_state, config, now)
state.animation_state.active = active
smoothing = _smoothing_config(state)
if not state.animation_state.active and smoothing is not None and smoothing.enabled:
state.displayed_value, _ = advance_smoothing(
state.smoothing_state, state.clamped_value, smoothing
)
def _redraw_placeholder(self) -> None:
state = self.state
if state.drawlist_tag is None or not dpg.does_item_exist(state.drawlist_tag):
return
width, height = effective_size(size_state_for_gauge(state))
width = width or max(state.config.width, 1) or 180
height = height or max(state.config.height, 1) or 80
dpg.configure_item(state.drawlist_tag, width=width, height=height, show=state.config.show)
dpg.configure_item(state.container_tag, show=state.config.show)
dpg.delete_item(state.drawlist_tag, children_only=True)
self.layers.clear()
style = state.config.style or GaugeStyle()
background = self.layers.background.add(f"{state.tag}__background")
frame = self.layers.static.add(f"{state.tag}__frame")
dpg.draw_rectangle(
(0, 0),
(width, height),
parent=state.drawlist_tag,
tag=background,
color=style.border_color,
fill=style.background_color,
rounding=6,
thickness=1,
)
dpg.draw_rectangle(
(4, 4),
(max(width - 4, 4), max(height - 4, 4)),
parent=state.drawlist_tag,
tag=frame,
color=style.frame_color,
rounding=4,
thickness=1,
)
label_y = 10
if state.config.show_label and state.config.label:
label = self.layers.text.add(f"{state.tag}__label")
dpg.draw_text(
(12, label_y),
state.config.label,
parent=state.drawlist_tag,
tag=label,
color=style.muted_text_color,
size=14,
)
value = self._display_text()
value_tag = self.layers.dynamic.add(f"{state.tag}__value")
dpg.draw_text(
(12, max(height * 0.45 - 10, 24)),
value,
parent=state.drawlist_tag,
tag=value_tag,
color=style.value_color if not state.invalid_value else style.redline_color,
size=24,
)
def _display_text(self) -> str:
state = self.state
if not state.config.show_value:
return ""
if state.invalid_value or state.displayed_value is None:
return "--"
if state.config.format_value is not None:
return state.config.format_value(state.displayed_value)
unit = state.config.unit if state.config.show_unit else ""
return format_gauge_value(state.displayed_value, state.config.precision, unit)
def render_attached_gauge(state: GaugeState) -> None:
if state.renderer is not None:
state.renderer.render()
def _smoothing_config(state: GaugeState) -> SmoothingConfig | None:
smoothing = state.config.smoothing
if isinstance(smoothing, bool):
return SmoothingConfig(enabled=smoothing)
return smoothing

View File

@@ -216,6 +216,7 @@ def set_runtime_value(tag: Tag, value: GaugeValue, *, now: float = 0.0) -> None:
state.displayed_value = clamped
state.smoothing_state.value = clamped
state.dirty |= DirtyFlags.VALUE | DirtyFlags.DYNAMIC | DirtyFlags.TEXT
render_runtime_gauge(state)
def get_runtime_value(tag: Tag) -> GaugeValue:
@@ -257,6 +258,7 @@ def update_runtime_gauge(tag: Tag, **config: Any) -> None:
dirty |= DirtyFlags.VALUE | DirtyFlags.DYNAMIC | DirtyFlags.TEXT
state.dirty |= dirty
render_runtime_gauge(state)
def set_runtime_thresholds(
@@ -278,10 +280,17 @@ def set_runtime_show(tag: Tag, show: bool) -> None:
def delete_runtime_gauge(tag: Tag) -> None:
get_gauge_state(tag)
state = get_gauge_state(tag)
if state.renderer is not None:
state.renderer.delete()
del _registry[tag]
def render_runtime_gauge(state: GaugeState) -> None:
if state.renderer is not None:
state.renderer.render()
def update_state_size(state: GaugeState, size: SizeState) -> None:
state.measured_width = size.measured_width
state.measured_height = size.measured_height

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)

View File

@@ -0,0 +1,78 @@
# pyright: reportGeneralTypeIssues=false
from collections.abc import Generator
import dearpygui.dearpygui as dpg
import pytest
import dpg_gauges as dpgg
from dpg_gauges.state import clear_registry, get_gauge_state
@pytest.fixture
def dpg_context() -> Generator[None]:
clear_registry()
dpgg.configure()
dpg.create_context()
try:
yield
finally:
clear_registry()
dpg.destroy_context()
def test_dpg_gauge_shell_registers_before_yield(dpg_context: None) -> None:
with (
dpg.window(label="Smoke"),
dpgg.digital_gauge(tag="speed", label="Speed", value=42, width=180, height=80) as config,
):
state = get_gauge_state("speed")
assert config.tag == "speed"
assert state.renderer is not None
assert dpg.does_item_exist(state.container_tag)
assert state.drawlist_tag is not None
assert dpg.does_item_exist(state.drawlist_tag)
def test_runtime_update_redraws_attached_gauge(dpg_context: None) -> None:
with dpg.window(label="Smoke"):
dpgg.digital_gauge(tag="rpm", label="RPM", value=1000, max_value=8000, width=200, height=90)
dpgg.set_value("rpm", 2500)
state = get_gauge_state("rpm")
debug = dpgg.get_gauge_debug_state("rpm")
assert state.displayed_value == 2500
assert state.dirty == 0
assert debug["renderer"]["drawlist_tag"] == "rpm__drawlist"
assert "rpm__value" in debug["renderer"]["layers"]["dynamic"]
def test_hidden_gauge_preserves_configured_size(dpg_context: None) -> None:
with dpg.window(label="Hidden"):
dpgg.digital_gauge(tag="hidden", value=1, width=160, height=70, show=False)
state = get_gauge_state("hidden")
assert state.measured_width == 160
assert state.measured_height == 70
assert state.last_nonzero_width == 160
assert state.last_nonzero_height == 70
dpgg.set_show("hidden", True)
assert dpg.does_item_exist(state.container_tag)
assert state.config.show is True
def test_panel_and_grid_are_lightweight_containers(dpg_context: None) -> None:
with (
dpg.window(label="Dashboard"),
dpgg.gauge_panel(tag="engine", label="Engine", width=300, height=180) as panel,
dpgg.gauge_grid(tag="engine_grid", columns=2) as grid,
):
assert panel == "engine"
assert grid == "engine_grid"
dpgg.digital_gauge(tag="temp", label="Temp", value=90, width=120, height=60)
assert dpg.does_item_exist("engine")
assert dpg.does_item_exist("engine_grid")
assert get_gauge_state("temp").renderer is not None