step 4: add digital bar level and status gauges

This commit is contained in:
2026-07-02 14:35:13 +02:00
parent 41e44a23bc
commit d638c512e9
11 changed files with 516 additions and 21 deletions

View File

@@ -2,9 +2,13 @@
Dear PyGui gauge widgets for dashboards and telemetry displays. Dear PyGui gauge widgets for dashboards and telemetry displays.
Current implementation status: Step 3 widget shells are implemented. Gauge creation functions still Current implementation status: Step 4 simple gauge rendering is implemented. Digital, bar, level,
register pure gauge state without requiring a Dear PyGui context, and when a Dear PyGui context is and status gauges render Dear PyGui drawlist visuals; bar and level gauges draw threshold bands and
active they create a group plus drawlist shell with placeholder frame and value text rendering. markers. 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.
Runtime value updates are intended for the Dear PyGui GUI thread. Attached gauges redraw immediately Runtime value updates are intended for the Dear PyGui GUI thread. Attached gauges redraw immediately
for simple value/configuration changes. for simple value/configuration changes.
Analog, segmented, compass, thermometer, battery, and multi-value visuals are scheduled for later
steps and may still use placeholder rendering.

View File

@@ -2,7 +2,7 @@
## Current status ## Current status
Step 3 completed and verified with automated checks. Ready to start Step 4. Step 4 completed and verified with automated checks. Step 4 commit is pending.
## Completed steps ## Completed steps
@@ -30,6 +30,15 @@ Step 1 - Public API contract and pure core:
Step 4 - Digital, bar, level, and status gauges. Step 4 - Digital, bar, level, and status gauges.
Implemented during Step 4:
- Added digital gauge drawlist rendering with formatted value text.
- Added bar and level gauge rendering with horizontal/vertical fill geometry.
- Added threshold band and marker drawing for bar/level gauges.
- Added status light rendering with configured status state colors and text.
- Added Dear PyGui item callback wiring for gauges with `callback=`.
- Added Step 4 examples and tests for geometry, runtime updates, thresholds, markers, and status states.
Step 3 - Widget shell and renderer foundation: Step 3 - Widget shell and renderer foundation:
Implemented during Step 3: Implemented during Step 3:
@@ -69,7 +78,7 @@ Implemented during Step 2:
## Known issues ## Known issues
Manual visual example checks were not run in this headless session. Manual visual example checks for Steps 3 and 4 were not run in this headless session.
## Commands used ## Commands used
@@ -102,7 +111,14 @@ Manual visual example checks were not run in this headless session.
- Ran `uv run ruff format .`. - Ran `uv run ruff format .`.
- Ran `uv run ruff format --check .`. - Ran `uv run ruff format --check .`.
- Ran `uv run pyright`. - Ran `uv run pyright`.
- Committed Step 3 with `git commit -m "step 3: add widget shell and renderer foundation"` before starting Step 4.
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 4 changes.
- Implemented Step 4 digital, bar, level, and status rendering, threshold/marker drawing, callback wiring, examples, README status update, and tests.
- Ran `uv run pytest`.
- Ran `uv run ruff check .`.
- Ran `uv run ruff format --check .`.
- Ran `uv run pyright`.
## Next action ## Next action
Implement Step 4. Commit Step 4, then implement Step 5.

33
examples/basic_bar.py Normal file
View File

@@ -0,0 +1,33 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Bar Gauge", width=360, height=180):
dpgg.bar_gauge(
tag="boost",
label="Boost",
value=18.5,
min_value=0,
max_value=30,
unit="psi",
precision=1,
width=300,
height=100,
)
dpg.create_viewport(title="dpg-gauges bar", width=400, height=220)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

30
examples/basic_level.py Normal file
View File

@@ -0,0 +1,30 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Level Gauge", width=260, height=320):
dpgg.level_gauge(
tag="fuel",
label="Fuel",
value=68,
unit="%",
width=180,
height=240,
)
dpg.create_viewport(title="dpg-gauges level", width=300, height=360)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

35
examples/basic_status.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:
states = {
"ok": dpgg.StatusState((0, 220, 120, 255), "OK"),
"warn": dpgg.StatusState((255, 190, 0, 255), "WARN"),
"fault": dpgg.StatusState((255, 45, 45, 255), "FAULT"),
}
with dpg.window(label="Status Light", width=300, height=160):
dpgg.status_light(
tag="ecu",
label="ECU",
state="ok",
states=states,
width=240,
height=80,
)
dpg.create_viewport(title="dpg-gauges status", width=340, height=200)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,37 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
thresholds = [
dpgg.ThresholdBand(0, 30, (60, 180, 90, 150), "normal"),
dpgg.ThresholdBand(70, 100, (255, 80, 45, 150), "hot"),
]
markers = [dpgg.GaugeMarker(85, (255, 255, 255, 255), "limit", 3)]
with dpg.window(label="Thresholds and Markers", width=420, height=220):
dpgg.bar_gauge(
tag="coolant",
label="Coolant",
value=76,
unit="C",
width=340,
height=100,
thresholds=thresholds,
markers=markers,
)
dpg.create_viewport(title="dpg-gauges thresholds", width=460, height=260)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

View File

@@ -1,20 +1,23 @@
"""Dear PyGui renderer foundation for gauge placeholders.""" """Dear PyGui renderer foundation for gauge drawlists."""
from __future__ import annotations from __future__ import annotations
import time import time
from typing import Any from typing import Any, cast
import dearpygui.dearpygui as dpg import dearpygui.dearpygui as dpg
from .animation import advance_animation from .animation import advance_animation
from .draw_layers import DrawLayers from .draw_layers import DrawLayers
from .scales import format_gauge_value from .gauges import BarGaugeConfig, LevelGaugeConfig, StatusLightConfig
from .scales import fill_fraction, fill_span, format_gauge_value, normalized_span
from .sizing import effective_size, update_measured_size from .sizing import effective_size, update_measured_size
from .smoothing import advance_smoothing from .smoothing import advance_smoothing
from .state import DirtyFlags, GaugeState, animation_config, size_state_for_gauge, update_state_size from .state import DirtyFlags, GaugeState, animation_config, size_state_for_gauge, update_state_size
from .themes import GaugeStyle from .themes import GaugeStyle
from .types import SmoothingConfig from .types import SmoothingConfig, StatusState, Tag
Rect = tuple[float, float, float, float]
class GaugeRenderer: class GaugeRenderer:
@@ -29,7 +32,7 @@ class GaugeRenderer:
state = self.state state = self.state
self._measure() self._measure()
self._advance_values() self._advance_values()
self._redraw_placeholder() self._redraw()
state.dirty = DirtyFlags.NONE state.dirty = DirtyFlags.NONE
if state.animation_state.active or state.smoothing_state.active: if state.animation_state.active or state.smoothing_state.active:
self.schedule_frame() self.schedule_frame()
@@ -63,7 +66,6 @@ class GaugeRenderer:
state = self.state state = self.state
if state.drawlist_tag is None or not dpg.does_item_exist(state.drawlist_tag): if state.drawlist_tag is None or not dpg.does_item_exist(state.drawlist_tag):
return return
width = dpg.get_item_width(state.drawlist_tag) or 0 width = dpg.get_item_width(state.drawlist_tag) or 0
height = dpg.get_item_height(state.drawlist_tag) or 0 height = dpg.get_item_height(state.drawlist_tag) or 0
size = size_state_for_gauge(state) size = size_state_for_gauge(state)
@@ -83,7 +85,7 @@ class GaugeRenderer:
state.smoothing_state, state.clamped_value, smoothing state.smoothing_state, state.clamped_value, smoothing
) )
def _redraw_placeholder(self) -> None: def _redraw(self) -> None:
state = self.state state = self.state
if state.drawlist_tag is None or not dpg.does_item_exist(state.drawlist_tag): if state.drawlist_tag is None or not dpg.does_item_exist(state.drawlist_tag):
return return
@@ -97,12 +99,25 @@ class GaugeRenderer:
self.layers.clear() self.layers.clear()
style = state.config.style or GaugeStyle() style = state.config.style or GaugeStyle()
self._draw_frame(width, height, style)
if state.gauge_type == "digital":
self._draw_digital(width, height, style)
elif state.gauge_type in {"bar", "level"}:
self._draw_bar_or_level(width, height, style)
elif state.gauge_type == "status_light":
self._draw_status_light(width, height, style)
else:
self._draw_placeholder_text(width, height, style)
def _draw_frame(self, width: int, height: int, style: GaugeStyle) -> None:
state = self.state
drawlist = _drawlist_tag(state)
background = self.layers.background.add(f"{state.tag}__background") background = self.layers.background.add(f"{state.tag}__background")
frame = self.layers.static.add(f"{state.tag}__frame") frame = self.layers.static.add(f"{state.tag}__frame")
dpg.draw_rectangle( dpg.draw_rectangle(
(0, 0), (0, 0),
(width, height), (width, height),
parent=state.drawlist_tag, parent=drawlist,
tag=background, tag=background,
color=style.border_color, color=style.border_color,
fill=style.background_color, fill=style.background_color,
@@ -112,36 +127,200 @@ class GaugeRenderer:
dpg.draw_rectangle( dpg.draw_rectangle(
(4, 4), (4, 4),
(max(width - 4, 4), max(height - 4, 4)), (max(width - 4, 4), max(height - 4, 4)),
parent=state.drawlist_tag, parent=drawlist,
tag=frame, tag=frame,
color=style.frame_color, color=style.frame_color,
rounding=4, rounding=4,
thickness=1, thickness=1,
) )
label_y = 10 def _draw_label(self, style: GaugeStyle, *, x: float = 12, y: float = 10) -> None:
state = self.state
if state.config.show_label and state.config.label: if state.config.show_label and state.config.label:
drawlist = _drawlist_tag(state)
label = self.layers.text.add(f"{state.tag}__label") label = self.layers.text.add(f"{state.tag}__label")
dpg.draw_text( dpg.draw_text(
(12, label_y), (x, y),
state.config.label, state.config.label,
parent=state.drawlist_tag, parent=drawlist,
tag=label, tag=label,
color=style.muted_text_color, color=style.muted_text_color,
size=14, size=14,
) )
value = self._display_text() def _draw_digital(self, _width: int, height: int, style: GaugeStyle) -> None:
self._draw_label(style)
state = self.state
drawlist = _drawlist_tag(state)
value_tag = self.layers.dynamic.add(f"{state.tag}__value")
size = max(min(height * 0.34, 34), 20)
dpg.draw_text(
(12, max((height - size) * 0.52, 26)),
self._display_text(),
parent=drawlist,
tag=value_tag,
color=style.value_color if not state.invalid_value else style.redline_color,
size=size,
)
def _draw_placeholder_text(self, _width: int, height: int, style: GaugeStyle) -> None:
self._draw_label(style)
state = self.state
drawlist = _drawlist_tag(state)
value_tag = self.layers.dynamic.add(f"{state.tag}__value") value_tag = self.layers.dynamic.add(f"{state.tag}__value")
dpg.draw_text( dpg.draw_text(
(12, max(height * 0.45 - 10, 24)), (12, max(height * 0.45 - 10, 24)),
value, self._display_text(),
parent=state.drawlist_tag, parent=drawlist,
tag=value_tag, tag=value_tag,
color=style.value_color if not state.invalid_value else style.redline_color, color=style.value_color if not state.invalid_value else style.redline_color,
size=24, size=24,
) )
def _draw_bar_or_level(self, width: int, height: int, style: GaugeStyle) -> None:
state = self.state
config = state.config
if not isinstance(config, BarGaugeConfig | LevelGaugeConfig):
return
self._draw_label(style)
bar = _bar_rect(width, height, config.orientation)
self._draw_bar_track(bar, style, config.corner_radius)
self._draw_thresholds(bar, config.orientation, config.corner_radius)
if state.displayed_value is not None and not state.invalid_value:
start, end = fill_span(
state.displayed_value,
config.min_value,
config.max_value,
fill_mode=config.fill_mode,
clamp=config.clamp,
)
self._draw_bar_fill(_oriented_rect(bar, start, end, config.orientation), style)
self._draw_markers(bar, config.orientation)
if config.show_value:
drawlist = _drawlist_tag(state)
value_tag = self.layers.text.add(f"{state.tag}__value")
dpg.draw_text(
(12, height - 24),
self._display_text(),
parent=drawlist,
tag=value_tag,
color=style.text_color,
size=14,
)
def _draw_bar_track(self, rect: Rect, style: GaugeStyle, rounding: float) -> None:
drawlist = _drawlist_tag(self.state)
tag = self.layers.static.add(f"{self.state.tag}__bar_track")
dpg.draw_rectangle(
(rect[0], rect[1]),
(rect[2], rect[3]),
parent=drawlist,
tag=tag,
color=style.border_color,
fill=style.frame_color,
rounding=rounding,
thickness=1,
)
def _draw_bar_fill(self, rect: Rect, style: GaugeStyle) -> None:
if rect[2] <= rect[0] or rect[3] <= rect[1]:
return
drawlist = _drawlist_tag(self.state)
tag = self.layers.dynamic.add(f"{self.state.tag}__bar_fill")
dpg.draw_rectangle(
(rect[0], rect[1]),
(rect[2], rect[3]),
parent=drawlist,
tag=tag,
color=style.value_color,
fill=style.value_color,
rounding=2,
thickness=1,
)
def _draw_thresholds(self, rect: Rect, orientation: str, rounding: float) -> None:
state = self.state
drawlist = _drawlist_tag(state)
for index, threshold in enumerate(state.config.thresholds):
start, end = normalized_span(
threshold.start, threshold.end, state.config.min_value, state.config.max_value
)
threshold_rect = _oriented_rect(rect, start, end, orientation)
if threshold_rect[2] <= threshold_rect[0] or threshold_rect[3] <= threshold_rect[1]:
continue
tag = self.layers.thresholds.add(f"{state.tag}__threshold_{index}")
dpg.draw_rectangle(
(threshold_rect[0], threshold_rect[1]),
(threshold_rect[2], threshold_rect[3]),
parent=drawlist,
tag=tag,
color=threshold.color,
fill=threshold.color,
rounding=rounding,
thickness=1,
)
def _draw_markers(self, rect: Rect, orientation: str) -> None:
state = self.state
drawlist = _drawlist_tag(state)
for index, marker in enumerate(state.config.markers):
fraction = fill_fraction(
marker.value, state.config.min_value, state.config.max_value, clamp=True
)
tag = self.layers.markers.add(f"{state.tag}__marker_{index}")
if orientation == "vertical":
y = rect[3] - (rect[3] - rect[1]) * fraction
dpg.draw_line(
(rect[0] - 4, y),
(rect[2] + 4, y),
parent=drawlist,
tag=tag,
color=marker.color,
thickness=marker.thickness,
)
else:
x = rect[0] + (rect[2] - rect[0]) * fraction
dpg.draw_line(
(x, rect[1] - 4),
(x, rect[3] + 4),
parent=drawlist,
tag=tag,
color=marker.color,
thickness=marker.thickness,
)
def _draw_status_light(self, width: int, height: int, style: GaugeStyle) -> None:
state = self.state
config = state.config
if not isinstance(config, StatusLightConfig):
return
drawlist = _drawlist_tag(state)
status = _resolve_status(config, style)
radius = max(min(width, height) * 0.18, 10)
center = (max(radius + 14, 28), height * 0.5)
light = self.layers.dynamic.add(f"{state.tag}__status_light")
dpg.draw_circle(
center,
radius,
parent=drawlist,
tag=light,
color=style.border_color,
fill=status.color,
thickness=2,
)
text = status.text or str(config.state)
if config.show_label and config.label:
text = f"{config.label}: {text}"
text_tag = self.layers.text.add(f"{state.tag}__status_text")
dpg.draw_text(
(center[0] + radius + 14, max(center[1] - 9, 8)),
text,
parent=drawlist,
tag=text_tag,
color=status.value_color or style.text_color,
size=18,
)
def _display_text(self) -> str: def _display_text(self) -> str:
state = self.state state = self.state
if not state.config.show_value: if not state.config.show_value:
@@ -164,3 +343,40 @@ def _smoothing_config(state: GaugeState) -> SmoothingConfig | None:
if isinstance(smoothing, bool): if isinstance(smoothing, bool):
return SmoothingConfig(enabled=smoothing) return SmoothingConfig(enabled=smoothing)
return smoothing return smoothing
def _drawlist_tag(state: GaugeState) -> Tag:
return cast(Tag, state.drawlist_tag)
def _bar_rect(width: int, height: int, orientation: str) -> Rect:
if orientation == "vertical":
top = 34.0
bottom = max(height - 34.0, top + 1.0)
bar_width = min(max(width * 0.34, 28.0), 56.0)
left = (width - bar_width) / 2.0
return left, top, left + bar_width, bottom
top = 36.0
bottom = max(height - 32.0, top + 1.0)
return 12.0, top, max(width - 12.0, 13.0), bottom
def _oriented_rect(rect: Rect, start: float, end: float, orientation: str) -> Rect:
start = min(max(start, 0.0), 1.0)
end = min(max(end, 0.0), 1.0)
if orientation == "vertical":
height = rect[3] - rect[1]
return rect[0], rect[3] - height * end, rect[2], rect[3] - height * start
width = rect[2] - rect[0]
return rect[0] + width * start, rect[1], rect[0] + width * end, rect[3]
def _resolve_status(config: StatusLightConfig, style: GaugeStyle) -> StatusState:
if config.states is not None and config.state in config.states:
return config.states[config.state]
if isinstance(config.state, bool):
return StatusState(
style.value_color if config.state else style.frame_color,
"ON" if config.state else "OFF",
)
return StatusState(style.value_color, str(config.state))

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import math import math
from collections.abc import Sequence from collections.abc import Sequence
from typing import Literal
from .exceptions import GaugeConfigError, GaugeValueError from .exceptions import GaugeConfigError, GaugeValueError
from .types import GaugeMarker, ThresholdBand from .types import GaugeMarker, ThresholdBand
@@ -59,6 +60,29 @@ def fill_fraction(value: float, min_value: float, max_value: float, *, clamp: bo
return normalize_value(value, min_value, max_value, clamp=clamp) return normalize_value(value, min_value, max_value, clamp=clamp)
def fill_span(
value: float,
min_value: float,
max_value: float,
*,
fill_mode: Literal["clamp", "centered"] = "clamp",
clamp: bool = True,
) -> tuple[float, float]:
fraction = fill_fraction(value, min_value, max_value, clamp=clamp)
if fill_mode == "clamp":
return 0.0, fraction
zero = fill_fraction(0.0, min_value, max_value, clamp=True)
return min(zero, fraction), max(zero, fraction)
def normalized_span(
start: float, end: float, min_value: float, max_value: float
) -> tuple[float, float]:
first = fill_fraction(start, min_value, max_value, clamp=True)
second = fill_fraction(end, min_value, max_value, clamp=True)
return min(first, second), max(first, second)
def generate_ticks(min_value: float, max_value: float, count: int) -> list[float]: def generate_ticks(min_value: float, max_value: float, count: int) -> list[float]:
validate_range(min_value, max_value) validate_range(min_value, max_value)
if count < 2: if count < 2:

View File

@@ -244,6 +244,8 @@ def update_runtime_gauge(tag: Tag, **config: Any) -> None:
dirty |= DirtyFlags.STATIC | DirtyFlags.DYNAMIC | DirtyFlags.TEXT dirty |= DirtyFlags.STATIC | DirtyFlags.DYNAMIC | DirtyFlags.TEXT
if any(name in config for name in ("thresholds", "markers")): if any(name in config for name in ("thresholds", "markers")):
dirty |= DirtyFlags.STATIC | DirtyFlags.DYNAMIC dirty |= DirtyFlags.STATIC | DirtyFlags.DYNAMIC
if any(name in config for name in ("state", "states")):
dirty |= DirtyFlags.DYNAMIC | DirtyFlags.TEXT
if any( if any(
name in config for name in ("label", "unit", "precision", "show_value", "show_unit") name in config for name in ("label", "unit", "precision", "show_value", "show_unit")
): ):

View File

@@ -132,6 +132,8 @@ def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[Confi
) )
renderer = GaugeRenderer(state) renderer = GaugeRenderer(state)
state.renderer = renderer state.renderer = renderer
if config.callback is not None:
dpg.configure_item(drawlist, callback=_item_callback, user_data=state.tag)
renderer.render() renderer.render()
return GaugeContext(state) return GaugeContext(state)
@@ -225,3 +227,11 @@ def _add_tooltip(parent: Tag, tooltip: str | None) -> None:
return return
tooltip_tag = dpg.add_tooltip(parent) tooltip_tag = dpg.add_tooltip(parent)
dpg.add_text(tooltip, parent=tooltip_tag) dpg.add_text(tooltip, parent=tooltip_tag)
def _item_callback(_sender: Tag, _app_data: object, user_data: Tag) -> None:
from .state import get_gauge_state
state = get_gauge_state(user_data)
if state.config.callback is not None:
state.config.callback(state.tag, state.target_value, state.config.user_data)

View File

@@ -0,0 +1,88 @@
# pyright: reportGeneralTypeIssues=false
from collections.abc import Generator
import dearpygui.dearpygui as dpg
import pytest
import dpg_gauges as dpgg
from dpg_gauges.gauges import LevelGaugeConfig, StatusLightConfig
from dpg_gauges.scales import fill_fraction, fill_span, normalized_span
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_bar_geometry_helpers_clamp_and_center() -> None:
assert fill_fraction(150, 0, 100, clamp=True) == 1.0
assert fill_span(25, 0, 100) == (0.0, 0.25)
assert fill_span(-25, -100, 100, fill_mode="centered") == (0.375, 0.5)
assert fill_span(25, -100, 100, fill_mode="centered") == (0.5, 0.625)
assert normalized_span(90, 10, 0, 100) == (0.1, 0.9)
def test_bar_runtime_renders_fill_thresholds_and_markers(dpg_context: None) -> None:
threshold = dpgg.ThresholdBand(35, 50, (255, 120, 0, 120))
marker = dpgg.GaugeMarker(50, (255, 255, 255, 255))
with dpg.window(label="Bar"):
dpgg.bar_gauge(
tag="boost",
label="Boost",
value=25,
min_value=0,
max_value=50,
width=240,
height=90,
thresholds=[threshold],
markers=[marker],
)
debug = dpgg.get_gauge_debug_state("boost")
assert "boost__bar_fill" in debug["renderer"]["layers"]["dynamic"]
assert "boost__threshold_0" in debug["renderer"]["layers"]["thresholds"]
assert "boost__marker_0" in debug["renderer"]["layers"]["markers"]
dpgg.set_value("boost", 100)
state = get_gauge_state("boost")
assert state.clamped_value == 50
assert state.displayed_value == 50
def test_level_runtime_uses_vertical_fill(dpg_context: None) -> None:
with dpg.window(label="Level"):
dpgg.level_gauge(tag="fuel", label="Fuel", value=75, width=120, height=180)
debug = dpgg.get_gauge_debug_state("fuel")
config = get_gauge_state("fuel").config
assert isinstance(config, LevelGaugeConfig)
assert config.orientation == "vertical"
assert "fuel__bar_fill" in debug["renderer"]["layers"]["dynamic"]
def test_status_light_uses_configured_state_text_and_color(dpg_context: None) -> None:
states = {
"ok": dpgg.StatusState((0, 220, 120, 255), "OK"),
"fault": dpgg.StatusState((255, 45, 45, 255), "FAULT"),
}
with dpg.window(label="Status"):
dpgg.status_light(tag="ecu", label="ECU", state="ok", states=states, width=180, height=70)
debug = dpgg.get_gauge_debug_state("ecu")
assert "ecu__status_light" in debug["renderer"]["layers"]["dynamic"]
assert "ecu__status_text" in debug["renderer"]["layers"]["text"]
dpgg.configure_gauge("ecu", state="fault")
config = get_gauge_state("ecu").config
assert isinstance(config, StatusLightConfig)
assert config.state == "fault"
assert get_gauge_state("ecu").dirty == 0