step 3: add widget shell and renderer foundation

This commit is contained in:
2026-07-02 14:20:22 +02:00
parent 34fd60c34e
commit 41e44a23bc
13 changed files with 683 additions and 84 deletions

View File

@@ -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