step 3: add widget shell and renderer foundation
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
"""Dear PyGui renderer foundation for gauge placeholders."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import dearpygui.dearpygui as dpg
|
||||
|
||||
from .animation import advance_animation
|
||||
from .draw_layers import DrawLayers
|
||||
from .scales import format_gauge_value
|
||||
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
|
||||
|
||||
|
||||
class GaugeRenderer:
|
||||
"""Small renderer that keeps a gauge drawlist populated between updates."""
|
||||
|
||||
def __init__(self, state: GaugeState) -> None:
|
||||
self.state = state
|
||||
self.layers = DrawLayers()
|
||||
self._scheduled = False
|
||||
|
||||
def render(self) -> None:
|
||||
state = self.state
|
||||
self._measure()
|
||||
self._advance_values()
|
||||
self._redraw_placeholder()
|
||||
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)
|
||||
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_placeholder(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()
|
||||
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,
|
||||
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=state.drawlist_tag,
|
||||
tag=frame,
|
||||
color=style.frame_color,
|
||||
rounding=4,
|
||||
thickness=1,
|
||||
)
|
||||
|
||||
label_y = 10
|
||||
if state.config.show_label and state.config.label:
|
||||
label = self.layers.text.add(f"{state.tag}__label")
|
||||
dpg.draw_text(
|
||||
(12, label_y),
|
||||
state.config.label,
|
||||
parent=state.drawlist_tag,
|
||||
tag=label,
|
||||
color=style.muted_text_color,
|
||||
size=14,
|
||||
)
|
||||
|
||||
value = self._display_text()
|
||||
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,
|
||||
tag=value_tag,
|
||||
color=style.value_color if not state.invalid_value else style.redline_color,
|
||||
size=24,
|
||||
)
|
||||
|
||||
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 _smoothing_config(state: GaugeState) -> SmoothingConfig | None:
|
||||
smoothing = state.config.smoothing
|
||||
if isinstance(smoothing, bool):
|
||||
return SmoothingConfig(enabled=smoothing)
|
||||
return smoothing
|
||||
|
||||
Reference in New Issue
Block a user