692 lines
25 KiB
Python
692 lines
25 KiB
Python
"""Dear PyGui renderer foundation for gauge drawlists."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
import time
|
|
from typing import Any, cast
|
|
|
|
import dearpygui.dearpygui as dpg
|
|
|
|
from .animation import advance_animation
|
|
from .draw_layers import DrawLayers
|
|
from .gauges import (
|
|
AnalogGaugeConfig,
|
|
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 .state import (
|
|
DirtyFlags,
|
|
GaugeState,
|
|
animation_config,
|
|
size_state_for_gauge,
|
|
smoothing_config,
|
|
update_state_size,
|
|
)
|
|
from .themes import GaugeStyle
|
|
from .types import StatusState, Tag
|
|
|
|
Rect = tuple[float, float, float, float]
|
|
|
|
|
|
class GaugeRenderer:
|
|
"""Small renderer that keeps a gauge drawlist populated between updates."""
|
|
|
|
def __init__(self, state: GaugeState) -> None:
|
|
self.state = state
|
|
self.layers: DrawLayers = DrawLayers()
|
|
self._scheduled = False
|
|
|
|
def render(self) -> None:
|
|
state = self.state
|
|
self._measure()
|
|
self._advance_values()
|
|
self._redraw()
|
|
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.config)
|
|
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(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()
|
|
self._draw_frame(width, height, style)
|
|
if state.gauge_type == "digital":
|
|
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"}:
|
|
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":
|
|
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=drawlist,
|
|
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=drawlist,
|
|
tag=frame,
|
|
color=style.frame_color,
|
|
rounding=4,
|
|
thickness=1,
|
|
)
|
|
|
|
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(
|
|
(x, y),
|
|
state.config.label,
|
|
parent=drawlist,
|
|
tag=label,
|
|
color=style.muted_text_color,
|
|
size=14,
|
|
)
|
|
|
|
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)),
|
|
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 _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:
|
|
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 _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 _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:
|
|
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))
|