633 lines
19 KiB
Python
633 lines
19 KiB
Python
"""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)
|