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

@@ -1,20 +1,23 @@
"""Dear PyGui renderer foundation for gauge placeholders."""
"""Dear PyGui renderer foundation for gauge drawlists."""
from __future__ import annotations
import time
from typing import Any
from typing import Any, cast
import dearpygui.dearpygui as dpg
from .animation import advance_animation
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 .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
from .types import SmoothingConfig, StatusState, Tag
Rect = tuple[float, float, float, float]
class GaugeRenderer:
@@ -29,7 +32,7 @@ class GaugeRenderer:
state = self.state
self._measure()
self._advance_values()
self._redraw_placeholder()
self._redraw()
state.dirty = DirtyFlags.NONE
if state.animation_state.active or state.smoothing_state.active:
self.schedule_frame()
@@ -63,7 +66,6 @@ class GaugeRenderer:
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)
@@ -83,7 +85,7 @@ class GaugeRenderer:
state.smoothing_state, state.clamped_value, smoothing
)
def _redraw_placeholder(self) -> None:
def _redraw(self) -> None:
state = self.state
if state.drawlist_tag is None or not dpg.does_item_exist(state.drawlist_tag):
return
@@ -97,12 +99,25 @@ class GaugeRenderer:
self.layers.clear()
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")
frame = self.layers.static.add(f"{state.tag}__frame")
dpg.draw_rectangle(
(0, 0),
(width, height),
parent=state.drawlist_tag,
parent=drawlist,
tag=background,
color=style.border_color,
fill=style.background_color,
@@ -112,36 +127,200 @@ class GaugeRenderer:
dpg.draw_rectangle(
(4, 4),
(max(width - 4, 4), max(height - 4, 4)),
parent=state.drawlist_tag,
parent=drawlist,
tag=frame,
color=style.frame_color,
rounding=4,
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:
drawlist = _drawlist_tag(state)
label = self.layers.text.add(f"{state.tag}__label")
dpg.draw_text(
(12, label_y),
(x, y),
state.config.label,
parent=state.drawlist_tag,
parent=drawlist,
tag=label,
color=style.muted_text_color,
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")
dpg.draw_text(
(12, max(height * 0.45 - 10, 24)),
value,
parent=state.drawlist_tag,
self._display_text(),
parent=drawlist,
tag=value_tag,
color=style.value_color if not state.invalid_value else style.redline_color,
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:
state = self.state
if not state.config.show_value:
@@ -164,3 +343,40 @@ def _smoothing_config(state: GaugeState) -> SmoothingConfig | None:
if isinstance(smoothing, bool):
return SmoothingConfig(enabled=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))