step 4: add digital bar level and status gauges
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user