Source code for simvx.editor.panels.play_controls
"""PlayControlBar — top-bar widget owning the editor's play controls.
Builds Play/Pause/Step/Stop/Reload buttons and keeps their visual state in
sync with :class:`State`. Owns the click handlers (state passthroughs
plus the single-frame Step and the persisted hot-reload toggle). Composed
by :class:`Root` into the top bar.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING
from simvx.core import HBoxContainer, ToolbarButton, Vec2
if TYPE_CHECKING:
from ..play_mode import PlayMode
from ..config import Config
from ..state import State
log = logging.getLogger(__name__)
_STEP_DT = 1.0 / 60.0
[docs]
class PlayControlBar(HBoxContainer):
"""Top-bar widget with Play/Pause/Step/Stop and a hot-reload toggle.
The bar drives :class:`State` for play lifecycle, calls
:class:`PlayMode` directly for the single-frame Step (which needs
bracketed pause/unpause), and persists the hot-reload preference
through :class:`Config`.
"""
PLAY_ACTIVE_BG = (0.15, 0.4, 0.15, 1.0)
PAUSE_ACTIVE_BG = (0.45, 0.35, 0.1, 1.0)
HOT_RELOAD_ACTIVE_BG = (0.18, 0.32, 0.5, 1.0)
def __init__(
self,
state: State,
play_mode: PlayMode,
prefs: Config,
*,
button_height: float = 24.0,
**kwargs,
):
kwargs.setdefault("name", "PlayControls")
super().__init__(**kwargs)
self.separation = 2
self.state = state
self.play_mode = play_mode
self.prefs = prefs
def _btn_size(text: str) -> Vec2:
return Vec2(max(50, len(text) * 9 + 16), button_height)
self.play_btn = ToolbarButton("Play", on_press=self._on_play, name="PlayButton")
self.play_btn.size = _btn_size("Play")
self.play_btn.tooltip = "Run the current scene (F5)"
self.pause_btn = ToolbarButton("Pause", on_press=self._on_pause, name="PauseButton")
self.pause_btn.size = _btn_size("Pause")
self.pause_btn.tooltip = "Pause / resume play mode (F7)"
self.pause_btn.disabled = True
self.step_btn = ToolbarButton("Step", on_press=self._on_step, name="StepButton")
self.step_btn.size = _btn_size("Step")
self.step_btn.tooltip = "Advance one frame (only while paused)"
self.step_btn.disabled = True
self.stop_btn = ToolbarButton("Stop", on_press=self._on_stop, name="StopButton")
self.stop_btn.size = _btn_size("Stop")
self.stop_btn.tooltip = "Stop play mode and restore the scene (F6)"
self.stop_btn.disabled = True
self.hot_reload_btn = ToolbarButton(
"Reload", on_press=self._on_toggle_hot_reload, name="HotReloadButton"
)
self.hot_reload_btn.size = _btn_size("Reload")
self.hot_reload_btn.tooltip = "Hot reload (live script updates during play)"
for btn in (self.play_btn, self.pause_btn, self.step_btn, self.stop_btn, self.hot_reload_btn):
self.add_child(btn)
state.play_state_changed.connect(self._sync_visual_state)
self._sync_visual_state()
# ------------------------------------------------------------------
# Handlers
# ------------------------------------------------------------------
def _on_play(self) -> None:
self.state.play_scene()
def _on_pause(self) -> None:
self.state.pause_scene()
def _on_stop(self) -> None:
self.state.stop_scene()
def _on_step(self) -> None:
"""Advance the game tree by exactly one frame while paused."""
if not (self.state.is_playing and self.state.is_paused):
return
self.state.is_paused = False
try:
self.play_mode.update(_STEP_DT)
finally:
self.state.is_paused = True
# Restore visual state without re-emitting play_state_changed.
self._sync_visual_state()
def _on_toggle_hot_reload(self) -> None:
"""Flip the hot-reload preference and persist to ``~/.config/simvx/config.json``."""
new_value = not self.state.hot_reload_enabled
self.play_mode.set_hot_reload_enabled(new_value)
self.prefs.config.editor.hot_reload_enabled = new_value
try:
self.prefs.save()
except OSError:
log.exception("Could not persist hot-reload preference")
self._sync_visual_state()
# ------------------------------------------------------------------
# Visual state
# ------------------------------------------------------------------
def _sync_visual_state(self) -> None:
"""Match button enabled/active flags to the current play state."""
playing = self.state.is_playing
paused = self.state.is_paused
self.play_btn.disabled = False
self.play_btn.active = playing
self.play_btn.active_bg_colour = self.PLAY_ACTIVE_BG if playing else None
self.play_btn.queue_redraw()
self.pause_btn.disabled = not playing
self.pause_btn.active = paused
self.pause_btn.active_bg_colour = self.PAUSE_ACTIVE_BG if paused else None
self.pause_btn.queue_redraw()
self.step_btn.disabled = not (playing and paused)
self.step_btn.active = False
self.step_btn.active_bg_colour = None
self.step_btn.queue_redraw()
self.stop_btn.disabled = not playing
self.stop_btn.active = False
self.stop_btn.active_bg_colour = None
self.stop_btn.queue_redraw()
enabled = bool(self.state.hot_reload_enabled)
self.hot_reload_btn.active = enabled
self.hot_reload_btn.active_bg_colour = self.HOT_RELOAD_ACTIVE_BG if enabled else None
self.hot_reload_btn.queue_redraw()