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()