"""Dear PyGui widget shell creation helpers.""" from __future__ import annotations from collections.abc import Mapping, Sequence from dataclasses import dataclass from types import TracebackType from typing import Any, Generic, Literal, TypeVar, cast import dearpygui.dearpygui as dpg from .gauges import ( AnalogGaugeConfig, BarGaugeConfig, BatteryGaugeConfig, CompassGaugeConfig, DigitalGaugeConfig, GaugeConfig, LevelGaugeConfig, MultiValueGaugeConfig, SegmentedGaugeConfig, StatusLightConfig, ThermometerGaugeConfig, ) from .renderer import GaugeRenderer from .state import GaugeState, register_gauge from .themes import GaugeStyle, GaugeTheme from .types import ( AnimationConfig, Callback, GaugeMarker, GaugeValue, SmoothingConfig, StatusState, Tag, ThresholdBand, ValueFormatter, ) ConfigT = TypeVar("ConfigT", bound=GaugeConfig) _dpg_context_active = False _original_create_context = dpg.create_context _original_destroy_context = dpg.destroy_context def _install_context_tracking() -> None: if getattr(dpg.create_context, "__dpg_gauges_wrapped__", False): return def create_context(*args: Any, **kwargs: Any) -> Any: global _dpg_context_active result = _original_create_context(*args, **kwargs) _dpg_context_active = True return result def destroy_context(*args: Any, **kwargs: Any) -> Any: global _dpg_context_active try: return _original_destroy_context(*args, **kwargs) finally: _dpg_context_active = False create_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined] destroy_context.__dpg_gauges_wrapped__ = True # type: ignore[attr-defined] dpg.create_context = create_context dpg.destroy_context = destroy_context _install_context_tracking() @dataclass class GaugeContext(Generic[ConfigT]): state: GaugeState def __enter__(self) -> ConfigT: if _dpg_context_active and dpg.does_item_exist(self.state.container_tag): dpg.push_container_stack(self.state.container_tag) return cast(ConfigT, self.state.config) def __exit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, traceback: TracebackType | None, ) -> None: if _dpg_context_active and dpg.does_item_exist(self.state.container_tag): dpg.pop_container_stack() @dataclass class LayoutContext: tag: Tag | None def __enter__(self) -> Tag | None: if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag): dpg.push_container_stack(self.tag) return self.tag def __exit__( self, exc_type: type[BaseException] | None, exc: BaseException | None, traceback: TracebackType | None, ) -> None: if self.tag is not None and _dpg_context_active and dpg.does_item_exist(self.tag): dpg.pop_container_stack() def create_gauge_context(gauge_type: str, config: ConfigT) -> GaugeContext[ConfigT]: if not _dpg_context_active: return GaugeContext(register_gauge(gauge_type, config)) tag = config.tag container_tag = tag if tag is not None else None drawlist_tag = f"{tag}__drawlist" if tag is not None else None width = config.width height = config.height or _default_height(gauge_type) if height == -1: height = _default_height(gauge_type) container = dpg.add_group( tag=container_tag or 0, width=width, height=height, show=config.show, user_data=config.user_data, ) drawlist = dpg.add_drawlist( max(width, 1) if width > 0 else _default_width(gauge_type), max(height, 1), tag=drawlist_tag or 0, parent=container, show=config.show, ) _add_tooltip(container, config.tooltip) state = register_gauge( gauge_type, config, container_tag=container, drawlist_tag=drawlist, text_tags={}, ) renderer = GaugeRenderer(state) state.renderer = renderer if config.callback is not None: dpg.configure_item(drawlist, callback=_item_callback, user_data=state.tag) renderer.render() return GaugeContext(state) def gauge_panel( *, tag: Tag | None = None, label: str | None = None, width: int = 0, height: int = 0, show: bool = True, border: bool = True, autosize_x: bool = False, autosize_y: bool = False, tooltip: str | None = None, **config: Any, ) -> LayoutContext: """Create a lightweight child-window container for related gauges.""" if not _dpg_context_active: return LayoutContext(None) container = dpg.add_child_window( tag=tag or 0, label=label or "", width=width, height=height, show=show, border=border, autosize_x=autosize_x, autosize_y=autosize_y, **config, ) _add_tooltip(container, tooltip) return LayoutContext(container) def gauge_grid( *, columns: int = 1, tag: Tag | None = None, width: int = 0, height: int = 0, show: bool = True, **config: Any, ) -> LayoutContext: """Create a lightweight horizontal gauge layout container.""" if not _dpg_context_active: return LayoutContext(None) columns = max(int(columns), 1) container = dpg.add_group( tag=tag or 0, width=width, height=height, show=show, horizontal=columns > 1, **config, ) return LayoutContext(container) def analog_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), start_angle: float = 225.0, end_angle: float = -45.0, major_ticks: int = 8, minor_ticks: int = 4, show_tick_labels: bool = True, needle_length: float = 0.82, needle_width: float = 3.0, center_cap_radius: float = 7.0, arc_width: float = 8.0, maintain_aspect: bool = True, redline_start: float | None = None, **config: Any, ) -> GaugeContext[AnalogGaugeConfig]: """Create an analog dial gauge with ticks, needle, thresholds, and markers.""" return create_gauge_context("analog", AnalogGaugeConfig(**_config_from_locals(locals()))) def digital_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), **config: Any, ) -> GaugeContext[DigitalGaugeConfig]: """Create a large numeric readout gauge.""" return create_gauge_context("digital", DigitalGaugeConfig(**_config_from_locals(locals()))) def bar_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), orientation: Literal["horizontal", "vertical"] = "horizontal", fill_mode: Literal["clamp", "centered"] = "clamp", corner_radius: float = 4.0, show_scale: bool = True, stretch: bool = True, **config: Any, ) -> GaugeContext[BarGaugeConfig]: """Create a horizontal or vertical fill bar gauge.""" return create_gauge_context("bar", BarGaugeConfig(**_config_from_locals(locals()))) def level_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), orientation: Literal["horizontal", "vertical"] = "vertical", fill_mode: Literal["clamp", "centered"] = "clamp", corner_radius: float = 4.0, show_scale: bool = True, stretch: bool = True, **config: Any, ) -> GaugeContext[LevelGaugeConfig]: """Create a tank or level-style vertical gauge.""" return create_gauge_context("level", LevelGaugeConfig(**_config_from_locals(locals()))) def status_light( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), state: str | bool | int = False, states: Mapping[Any, StatusState] | None = None, blink: bool = False, pulse: bool = False, **config: Any, ) -> GaugeContext[StatusLightConfig]: """Create an LED-style status indicator gauge.""" return create_gauge_context("status_light", StatusLightConfig(**_config_from_locals(locals()))) def segmented_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), orientation: Literal["horizontal", "vertical"] = "horizontal", fill_mode: Literal["clamp", "centered"] = "clamp", corner_radius: float = 4.0, show_scale: bool = True, stretch: bool = True, segments: int = 10, **config: Any, ) -> GaugeContext[SegmentedGaugeConfig]: """Create a segmented strip gauge for shift-light or cell-style displays.""" return create_gauge_context("segmented", SegmentedGaugeConfig(**_config_from_locals(locals()))) def compass_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 360.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), start_angle: float = 90.0, end_angle: float = -270.0, major_ticks: int = 8, minor_ticks: int = 4, show_tick_labels: bool = True, needle_length: float = 0.82, needle_width: float = 3.0, center_cap_radius: float = 7.0, arc_width: float = 8.0, maintain_aspect: bool = True, redline_start: float | None = None, **config: Any, ) -> GaugeContext[CompassGaugeConfig]: """Create a compass gauge that wraps numeric headings to 0-360 degrees.""" return create_gauge_context("compass", CompassGaugeConfig(**_config_from_locals(locals()))) def thermometer_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), orientation: Literal["horizontal", "vertical"] = "vertical", fill_mode: Literal["clamp", "centered"] = "clamp", corner_radius: float = 4.0, show_scale: bool = True, stretch: bool = True, **config: Any, ) -> GaugeContext[ThermometerGaugeConfig]: """Create a thermometer-style temperature gauge.""" return create_gauge_context( "thermometer", ThermometerGaugeConfig(**_config_from_locals(locals())) ) def battery_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), orientation: Literal["horizontal", "vertical"] = "vertical", fill_mode: Literal["clamp", "centered"] = "clamp", corner_radius: float = 4.0, show_scale: bool = True, stretch: bool = True, charging: bool | None = None, **config: Any, ) -> GaugeContext[BatteryGaugeConfig]: """Create a battery-style percentage or value gauge.""" return create_gauge_context("battery", BatteryGaugeConfig(**_config_from_locals(locals()))) def multi_value_gauge( *, tag: Tag | None = None, label: str | None = None, value: GaugeValue = None, min_value: float = 0.0, max_value: float = 100.0, unit: str = "", precision: int = 0, width: int = 0, height: int = 0, autosize_x: bool = False, autosize_y: bool = False, show: bool = True, show_label: bool = True, show_value: bool = True, show_unit: bool = True, tooltip: str | None = None, callback: Callback | None = None, user_data: Any = None, theme: str | GaugeTheme | None = None, style: GaugeStyle | None = None, animation: AnimationConfig | bool | None = None, smoothing: SmoothingConfig | bool | None = None, clamp: bool = True, format_value: ValueFormatter | None = None, thresholds: Sequence[ThresholdBand] = (), markers: Sequence[GaugeMarker] = (), values: Mapping[str, GaugeValue] | None = None, **config: Any, ) -> GaugeContext[MultiValueGaugeConfig]: """Create a compact grouped key/value readout gauge.""" return create_gauge_context( "multi_value", MultiValueGaugeConfig(**_config_from_locals(locals())) ) def _config_from_locals(values: dict[str, Any]) -> dict[str, Any]: config = dict(values.pop("config")) config.update(values) return config def _default_width(gauge_type: str) -> int: return 220 if gauge_type in {"analog", "compass"} else 180 def _default_height(gauge_type: str) -> int: return 220 if gauge_type in {"analog", "compass"} else 80 def _add_tooltip(parent: Tag, tooltip: str | None) -> None: if not tooltip: return tooltip_tag = dpg.add_tooltip(parent) dpg.add_text(tooltip, parent=tooltip_tag) def _item_callback(_sender: Tag, _app_data: object, user_data: Tag) -> None: from .state import get_gauge_state state = get_gauge_state(user_data) if state.config.callback is not None: state.config.callback(state.tag, state.target_value, state.config.user_data)