step 5: complete first gauge catalogue

This commit is contained in:
2026-07-02 14:47:05 +02:00
parent d638c512e9
commit 1a476bc91d
11 changed files with 594 additions and 16 deletions

View File

@@ -2,13 +2,10 @@
Dear PyGui gauge widgets for dashboards and telemetry displays. Dear PyGui gauge widgets for dashboards and telemetry displays.
Current implementation status: Step 4 simple gauge rendering is implemented. Digital, bar, level, Current implementation status: Step 5 first catalogue rendering is implemented. Analog, digital,
and status gauges render Dear PyGui drawlist visuals; bar and level gauges draw threshold bands and bar, level, status, segmented, compass, thermometer, battery, and multi-value gauges render Dear
markers. Gauge creation functions still register pure gauge state without requiring a Dear PyGui PyGui drawlist visuals. Bar/level gauges draw threshold bands and markers; analog gauges draw ticks,
context, and when a Dear PyGui context is active they create a group plus drawlist shell. redline bands, markers, and needles.
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 4 completed and verified with automated checks. Step 4 commit is pending. Step 5 completed and verified with automated checks. Step 5 commit is pending.
## Completed steps ## Completed steps
@@ -28,7 +28,20 @@ Step 1 - Public API contract and pure core:
## Current step ## Current step
Step 4 - Digital, bar, level, and status gauges. Step 6 - High-rate updates, animation, and stress examples.
Step 5 - Analog, segmented, compass, thermometer, battery, and multi-value gauges:
Implemented during Step 5:
- Added analog and compass dial rendering with arcs, ticks, tick labels, needles, redline bands, and markers.
- Added compass heading wraparound for runtime values.
- Added segmented gauge rendering with active segment calculation.
- Added thermometer and battery-specific drawlist rendering.
- Added multi-value gauge rendering.
- Added Step 5 examples and tests for angle math, tick generation, compass wraparound, segment activation, battery clamp, and multi-value rendering.
Step 4 - Digital, bar, level, and status gauges:
Implemented during Step 4: Implemented during Step 4:
@@ -78,7 +91,7 @@ Implemented during Step 2:
## Known issues ## Known issues
Manual visual example checks for Steps 3 and 4 were not run in this headless session. Manual visual example checks for Steps 3, 4, and 5 were not run in this headless session.
## Commands used ## Commands used
@@ -97,6 +110,14 @@ Manual visual example checks for Steps 3 and 4 were not run in this headless ses
- Ran `uv run ruff check .`. - Ran `uv run ruff check .`.
- Ran `uv run ruff format --check .`. - Ran `uv run ruff format --check .`.
- Ran `uv run pyright`. - Ran `uv run pyright`.
- Committed Step 4 with `git commit -m "step 4: add digital bar level and status gauges"` before starting Step 5.
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 5 changes.
- Implemented Step 5 first catalogue rendering, compass wraparound, examples, README status update, and tests.
- Ran `uv run pytest`.
- Ran `uv run ruff check .`.
- Ran `uv run ruff format .`.
- Ran `uv run ruff format --check .`.
- Ran `uv run pyright`.
- Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 2 changes. - Read `codex/FEATURES.md`, `codex/ARCHITECTURE.md`, `codex/STEPS.md`, and `codex/AGENTS.md` before Step 2 changes.
- Implemented Step 2 state, sizing, animation, smoothing, runtime API, diagnostics, tests, and README status update. - Implemented Step 2 state, sizing, animation, smoothing, runtime API, diagnostics, tests, and README status update.
- Ran `uv run pytest`. - Ran `uv run pytest`.
@@ -121,4 +142,4 @@ Manual visual example checks for Steps 3 and 4 were not run in this headless ses
## Next action ## Next action
Commit Step 4, then implement Step 5. Commit Step 5, then implement Step 6.

25
examples/basic_battery.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="Battery Gauge", width=320, height=180):
dpgg.battery_gauge(
tag="battery", label="Battery", value=82, unit="%", width=260, height=110
)
dpg.create_viewport(title="dpg-gauges battery", width=360, height=220)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

23
examples/basic_compass.py Normal file
View File

@@ -0,0 +1,23 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Compass Gauge", width=320, height=320):
dpgg.compass_gauge(tag="heading", label="Heading", value=275, width=240, height=240)
dpg.create_viewport(title="dpg-gauges compass", width=360, height=360)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,29 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Multi Value Gauge", width=340, height=220):
dpgg.multi_value_gauge(
tag="engine",
label="Engine",
values={"AFR": 12.8, "Oil": "58 psi", "Fuel": "43 psi"},
width=280,
height=150,
)
dpg.create_viewport(title="dpg-gauges multi-value", width=380, height=260)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,32 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Segmented Gauge", width=360, height=160):
dpgg.segmented_gauge(
tag="shift",
label="Shift lights",
value=7200,
min_value=0,
max_value=8000,
segments=12,
width=300,
height=90,
)
dpg.create_viewport(title="dpg-gauges segmented", width=400, height=200)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,32 @@
# pyright: reportGeneralTypeIssues=false
import dearpygui.dearpygui as dpg
import dpg_gauges as dpgg
def main() -> None:
dpg.create_context()
try:
with dpg.window(label="Thermometer Gauge", width=280, height=340):
dpgg.thermometer_gauge(
tag="coolant",
label="Coolant",
value=92,
min_value=40,
max_value=120,
unit="C",
width=180,
height=260,
)
dpg.create_viewport(title="dpg-gauges thermometer", width=320, height=380)
dpg.setup_dearpygui()
dpg.show_viewport()
dpg.start_dearpygui()
finally:
dpg.destroy_context()
if __name__ == "__main__":
main()

View File

@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
import math
import time import time
from typing import Any, cast from typing import Any, cast
@@ -9,9 +10,26 @@ 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 .gauges import BarGaugeConfig, LevelGaugeConfig, StatusLightConfig from .gauges import (
from .scales import fill_fraction, fill_span, format_gauge_value, normalized_span AnalogGaugeConfig,
from .sizing import effective_size, update_measured_size BarGaugeConfig,
BatteryGaugeConfig,
CompassGaugeConfig,
LevelGaugeConfig,
MultiValueGaugeConfig,
SegmentedGaugeConfig,
StatusLightConfig,
)
from .scales import (
active_segments,
fill_fraction,
fill_span,
format_gauge_value,
generate_ticks,
normalized_span,
value_to_angle,
)
from .sizing import effective_size, square_gauge_rect, 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
@@ -102,8 +120,18 @@ class GaugeRenderer:
self._draw_frame(width, height, style) self._draw_frame(width, height, style)
if state.gauge_type == "digital": if state.gauge_type == "digital":
self._draw_digital(width, height, style) self._draw_digital(width, height, style)
elif state.gauge_type in {"analog", "compass"}:
self._draw_analog(width, height, style)
elif state.gauge_type in {"bar", "level"}: elif state.gauge_type in {"bar", "level"}:
self._draw_bar_or_level(width, height, style) self._draw_bar_or_level(width, height, style)
elif state.gauge_type == "segmented":
self._draw_segmented(width, height, style)
elif state.gauge_type == "thermometer":
self._draw_thermometer(width, height, style)
elif state.gauge_type == "battery":
self._draw_battery(width, height, style)
elif state.gauge_type == "multi_value":
self._draw_multi_value(width, height, style)
elif state.gauge_type == "status_light": elif state.gauge_type == "status_light":
self._draw_status_light(width, height, style) self._draw_status_light(width, height, style)
else: else:
@@ -321,6 +349,280 @@ class GaugeRenderer:
size=18, size=18,
) )
def _draw_analog(self, width: int, height: int, style: GaugeStyle) -> None:
state = self.state
config = state.config
if not isinstance(config, AnalogGaugeConfig | CompassGaugeConfig):
return
drawlist = _drawlist_tag(state)
rect = square_gauge_rect(width - 16, height - 16)
cx = 8 + rect.x + rect.width / 2.0
cy = 8 + rect.y + rect.height / 2.0
radius = max(min(rect.width, rect.height) * 0.43, 12.0)
if config.show_label and config.label:
self._draw_label(style, x=12, y=10)
self._draw_arc(
(cx, cy),
radius,
config.start_angle,
config.end_angle,
style.frame_color,
config.arc_width,
)
for threshold in config.thresholds:
start = value_to_angle(
threshold.start,
config.min_value,
config.max_value,
config.start_angle,
config.end_angle,
)
end = value_to_angle(
threshold.end,
config.min_value,
config.max_value,
config.start_angle,
config.end_angle,
)
self._draw_arc((cx, cy), radius, start, end, threshold.color, config.arc_width + 2)
if config.redline_start is not None:
start = value_to_angle(
config.redline_start,
config.min_value,
config.max_value,
config.start_angle,
config.end_angle,
)
self._draw_arc(
(cx, cy), radius, start, config.end_angle, style.redline_color, config.arc_width + 3
)
ticks = generate_ticks(config.min_value, config.max_value, max(config.major_ticks, 2))
for index, tick in enumerate(ticks):
angle = value_to_angle(
tick, config.min_value, config.max_value, config.start_angle, config.end_angle
)
outer = _point_on_circle(cx, cy, radius + 4, angle)
inner = _point_on_circle(cx, cy, radius - 10, angle)
tag = self.layers.static.add(f"{state.tag}__tick_{index}")
dpg.draw_line(
outer, inner, parent=drawlist, tag=tag, color=style.tick_color, thickness=2
)
if config.show_tick_labels:
label_pos = _point_on_circle(cx, cy, radius - 26, angle)
label = self.layers.text.add(f"{state.tag}__tick_label_{index}")
text = f"{tick:.0f}"
dpg.draw_text(
(label_pos[0] - 10, label_pos[1] - 6),
text,
parent=drawlist,
tag=label,
color=style.muted_text_color,
size=10,
)
for index, marker in enumerate(config.markers):
angle = value_to_angle(
marker.value,
config.min_value,
config.max_value,
config.start_angle,
config.end_angle,
)
outer = _point_on_circle(cx, cy, radius + 8, angle)
inner = _point_on_circle(cx, cy, radius - 18, angle)
tag = self.layers.markers.add(f"{state.tag}__marker_{index}")
dpg.draw_line(
outer,
inner,
parent=drawlist,
tag=tag,
color=marker.color,
thickness=marker.thickness,
)
if state.displayed_value is not None and not state.invalid_value:
angle = value_to_angle(
state.displayed_value,
config.min_value,
config.max_value,
config.start_angle,
config.end_angle,
)
end = _point_on_circle(cx, cy, radius * config.needle_length, angle)
needle = self.layers.dynamic.add(f"{state.tag}__needle")
dpg.draw_line(
(cx, cy),
end,
parent=drawlist,
tag=needle,
color=style.needle_color,
thickness=config.needle_width,
)
cap = self.layers.dynamic.add(f"{state.tag}__center_cap")
dpg.draw_circle(
(cx, cy),
config.center_cap_radius,
parent=drawlist,
tag=cap,
color=style.border_color,
fill=style.value_color,
)
if config.show_value:
value = self.layers.text.add(f"{state.tag}__value")
dpg.draw_text(
(cx - radius * 0.45, cy + radius * 0.38),
self._display_text(),
parent=drawlist,
tag=value,
color=style.text_color,
size=14,
)
def _draw_arc(
self,
center: tuple[float, float],
radius: float,
start_angle: float,
end_angle: float,
color: tuple[int, ...],
thickness: float,
) -> None:
drawlist = _drawlist_tag(self.state)
steps = max(int(abs(end_angle - start_angle) / 8), 2)
previous = _point_on_circle(center[0], center[1], radius, start_angle)
for index in range(1, steps + 1):
angle = start_angle + (end_angle - start_angle) * (index / steps)
current = _point_on_circle(center[0], center[1], radius, angle)
tag = self.layers.static.add(
f"{self.state.tag}__arc_{len(self.layers.static.item_tags)}"
)
dpg.draw_line(
previous, current, parent=drawlist, tag=tag, color=color, thickness=thickness
)
previous = current
def _draw_segmented(self, width: int, height: int, style: GaugeStyle) -> None:
state = self.state
config = state.config
if not isinstance(config, SegmentedGaugeConfig):
return
self._draw_label(style)
drawlist = _drawlist_tag(state)
count = config.segments
active = 0
if state.displayed_value is not None and not state.invalid_value:
active = active_segments(
state.displayed_value, config.min_value, config.max_value, count
)
gap = 4.0
left, top, right, bottom = 12.0, 38.0, max(width - 12.0, 13.0), max(height - 28.0, 39.0)
segment_width = max((right - left - gap * (count - 1)) / count, 1.0)
for index in range(count):
x1 = left + index * (segment_width + gap)
x2 = x1 + segment_width
color = style.value_color if index < active else style.frame_color
tag = self.layers.dynamic.add(f"{state.tag}__segment_{index}")
dpg.draw_rectangle(
(x1, top),
(x2, bottom),
parent=drawlist,
tag=tag,
color=style.border_color,
fill=color,
rounding=config.corner_radius,
)
def _draw_thermometer(self, width: int, height: int, style: GaugeStyle) -> None:
self._draw_bar_or_level(width, height, style)
drawlist = _drawlist_tag(self.state)
bulb = self.layers.dynamic.add(f"{self.state.tag}__bulb")
dpg.draw_circle(
(width / 2.0, max(height - 30.0, 18.0)),
16,
parent=drawlist,
tag=bulb,
color=style.border_color,
fill=style.value_color,
)
def _draw_battery(self, width: int, height: int, style: GaugeStyle) -> None:
state = self.state
config = state.config
if not isinstance(config, BatteryGaugeConfig):
return
self._draw_label(style)
drawlist = _drawlist_tag(state)
body = (18.0, 40.0, max(width - 34.0, 58.0), max(height - 26.0, 74.0))
nub = (
body[2],
body[1] + (body[3] - body[1]) * 0.33,
body[2] + 10,
body[1] + (body[3] - body[1]) * 0.67,
)
dpg.draw_rectangle(
(body[0], body[1]),
(body[2], body[3]),
parent=drawlist,
tag=self.layers.static.add(f"{state.tag}__battery_body"),
color=style.border_color,
thickness=2,
rounding=3,
)
dpg.draw_rectangle(
(nub[0], nub[1]),
(nub[2], nub[3]),
parent=drawlist,
tag=self.layers.static.add(f"{state.tag}__battery_nub"),
color=style.border_color,
fill=style.border_color,
rounding=2,
)
if state.displayed_value is not None and not state.invalid_value:
fraction = fill_fraction(
state.displayed_value, config.min_value, config.max_value, clamp=True
)
pad = 5.0
fill = (
body[0] + pad,
body[1] + pad,
body[0] + pad + (body[2] - body[0] - pad * 2) * fraction,
body[3] - pad,
)
if fill[2] > fill[0]:
dpg.draw_rectangle(
(fill[0], fill[1]),
(fill[2], fill[3]),
parent=drawlist,
tag=self.layers.dynamic.add(f"{state.tag}__battery_fill"),
color=style.value_color,
fill=style.value_color,
rounding=2,
)
if config.show_value:
dpg.draw_text(
(18, height - 22),
self._display_text(),
parent=drawlist,
tag=self.layers.text.add(f"{state.tag}__value"),
color=style.text_color,
size=14,
)
def _draw_multi_value(self, _width: int, height: int, style: GaugeStyle) -> None:
state = self.state
config = state.config
if not isinstance(config, MultiValueGaugeConfig):
return
self._draw_label(style)
drawlist = _drawlist_tag(state)
values = config.values or {}
y = 34.0
for index, (name, value) in enumerate(values.items()):
text = f"{name}: {value}"
tag = self.layers.text.add(f"{state.tag}__multi_{index}")
dpg.draw_text((12, y), text, parent=drawlist, tag=tag, color=style.text_color, size=16)
y += 22.0
if y > height - 18:
break
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:
@@ -371,6 +673,13 @@ def _oriented_rect(rect: Rect, start: float, end: float, orientation: str) -> Re
return rect[0] + width * start, rect[1], rect[0] + width * end, rect[3] return rect[0] + width * start, rect[1], rect[0] + width * end, rect[3]
def _point_on_circle(
cx: float, cy: float, radius: float, angle_degrees: float
) -> tuple[float, float]:
radians = math.radians(angle_degrees)
return cx + math.cos(radians) * radius, cy - math.sin(radians) * radius
def _resolve_status(config: StatusLightConfig, style: GaugeStyle) -> StatusState: def _resolve_status(config: StatusLightConfig, style: GaugeStyle) -> StatusState:
if config.states is not None and config.state in config.states: if config.states is not None and config.state in config.states:
return config.states[config.state] return config.states[config.state]

View File

@@ -56,6 +56,13 @@ def value_to_angle(
return start_angle + (end_angle - start_angle) * fraction return start_angle + (end_angle - start_angle) * fraction
def compass_heading(value: float) -> float:
if not math.isfinite(value):
msg = "heading value must be finite"
raise GaugeValueError(msg)
return value % 360.0
def fill_fraction(value: float, min_value: float, max_value: float, *, clamp: bool = True) -> float: def fill_fraction(value: float, min_value: float, max_value: float, *, clamp: bool = True) -> float:
return normalize_value(value, min_value, max_value, clamp=clamp) return normalize_value(value, min_value, max_value, clamp=clamp)
@@ -83,6 +90,16 @@ def normalized_span(
return min(first, second), max(first, second) return min(first, second), max(first, second)
def active_segments(
value: float, min_value: float, max_value: float, segments: int, *, clamp: bool = True
) -> int:
if segments <= 0:
msg = "segments must be greater than zero"
raise GaugeConfigError(msg)
fraction = fill_fraction(value, min_value, max_value, clamp=clamp)
return min(max(math.ceil(fraction * segments), 0), segments)
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

@@ -9,8 +9,8 @@ from typing import Any
from .animation import AnimationState, start_animation from .animation import AnimationState, start_animation
from .exceptions import GaugeConfigError, GaugeNotFoundError from .exceptions import GaugeConfigError, GaugeNotFoundError
from .gauges import GaugeConfig from .gauges import CompassGaugeConfig, GaugeConfig
from .scales import clamp_value, validate_markers, validate_thresholds from .scales import clamp_value, compass_heading, validate_markers, validate_thresholds
from .sizing import RequestedSize, SizeState from .sizing import RequestedSize, SizeState
from .smoothing import SmoothingState from .smoothing import SmoothingState
from .themes import GaugeTheme, get_theme from .themes import GaugeTheme, get_theme
@@ -150,6 +150,8 @@ def clamp_runtime_value(value: GaugeValue, config: GaugeConfig) -> tuple[float |
return None, value is not None return None, value is not None
if not math.isfinite(numeric): if not math.isfinite(numeric):
return None, True return None, True
if isinstance(config, CompassGaugeConfig):
numeric = compass_heading(numeric)
if config.clamp: if config.clamp:
return clamp_value(numeric, config.min_value, config.max_value), False return clamp_value(numeric, config.min_value, config.max_value), False
return numeric, False return numeric, False

View File

@@ -0,0 +1,91 @@
# pyright: reportGeneralTypeIssues=false
from collections.abc import Generator
import dearpygui.dearpygui as dpg
import pytest
import dpg_gauges as dpgg
from dpg_gauges.scales import active_segments, compass_heading, generate_ticks, value_to_angle
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_value_to_angle_ticks_and_compass_wrap() -> None:
assert value_to_angle(50, 0, 100, 225, -45) == 90
assert generate_ticks(0, 100, 3) == [0, 50, 100]
assert compass_heading(370) == 10
assert compass_heading(-10) == 350
def test_analog_renders_needle_redline_and_marker(dpg_context: None) -> None:
marker = dpgg.GaugeMarker(6000, (255, 255, 255, 255))
with dpg.window(label="Analog"):
dpgg.analog_gauge(
tag="rpm",
label="RPM",
value=3500,
max_value=8000,
redline_start=6500,
markers=[marker],
width=240,
height=200,
)
debug = dpgg.get_gauge_debug_state("rpm")
assert "rpm__needle" in debug["renderer"]["layers"]["dynamic"]
assert "rpm__marker_0" in debug["renderer"]["layers"]["markers"]
assert get_gauge_state("rpm").measured_width == 240
def test_compass_wraps_runtime_heading(dpg_context: None) -> None:
with dpg.window(label="Compass"):
dpgg.compass_gauge(tag="heading", value=370, width=200, height=200)
state = get_gauge_state("heading")
assert state.clamped_value == 10
assert (
"heading__needle" in dpgg.get_gauge_debug_state("heading")["renderer"]["layers"]["dynamic"]
)
def test_segmented_activation_and_rendering(dpg_context: None) -> None:
assert active_segments(55, 0, 100, 10) == 6
with dpg.window(label="Segments"):
dpgg.segmented_gauge(tag="shift", value=55, segments=10, width=260, height=80)
dynamic = dpgg.get_gauge_debug_state("shift")["renderer"]["layers"]["dynamic"]
assert "shift__segment_0" in dynamic
assert "shift__segment_9" in dynamic
def test_battery_clamps_and_multi_value_renders(dpg_context: None) -> None:
with dpg.window(label="Catalogue"):
dpgg.battery_gauge(tag="battery", value=125, width=220, height=100)
dpgg.multi_value_gauge(
tag="engine",
label="Engine",
values={"AFR": 12.8, "Oil": "58 psi"},
width=220,
height=110,
)
assert get_gauge_state("battery").clamped_value == 100
assert (
"battery__battery_fill"
in dpgg.get_gauge_debug_state("battery")["renderer"]["layers"]["dynamic"]
)
text = dpgg.get_gauge_debug_state("engine")["renderer"]["layers"]["text"]
assert "engine__multi_0" in text
assert "engine__multi_1" in text