from st3m.reactor import Responder from st3m.goose import ABCBase, abstractmethod, Optional, List from st3m.input import InputState, InputController from st3m.ui.ctx import Ctx class View(Responder): """ A View extends a reactor Responder with callbacks related to the Responder's lifecycle in terms of being foregrounded or backgrounded. These signals can be used to alter input processing, se the ViewWithInputState class. """ def on_enter(self) -> None: """ Called when the View has just become active. This is guaranteed to be called before think(). """ pass class ViewWithInputState(View): """ A base class helper for implementing views which respond to inputs and who want to do their own, separate input processing. Derive this class, then use self.input to access the InputController. Remember to call super().think() in think()! """ __slots__ = ("input",) def __init__(self) -> None: self.input = InputController() def on_enter(self) -> None: self.input._ignore_pressed() def think(self, ins: InputState, delta_ms: int) -> None: self.input.think(ins, delta_ms) class ViewTransition(ABCBase): """ A transition from one View/Responder to another. Can be implemented by the user to provide transition animations. """ @abstractmethod def draw( self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder ) -> None: """ Called when the ViewManager performs a transition from the outgoing responder to the incoming responder. The implementer should draw both Responders when appropriate. The transition value is a float from 0 to 1 which represents the progress of the transition. """ pass class ViewTransitionBlend(ViewTransition): """ Transition from one view to another by opacity blending. """ def draw( self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder ) -> None: ctx.start_group() outgoing.draw(ctx) ctx.end_group() ctx.start_group() ctx.global_alpha = transition incoming.draw(ctx) ctx.end_group() class ViewTransitionSwipeLeft(ViewTransition): """ Swipe the outoing view to the left and replace it with the incoming view. """ def draw( self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder ) -> None: ctx.save() ctx.translate(transition * -240, 0) outgoing.draw(ctx) ctx.restore() ctx.save() ctx.translate(240 + transition * -240, 0) incoming.draw(ctx) ctx.restore() class ViewTransitionSwipeRight(ViewTransition): """ Swipe the outoing view to the right and replace it with the incoming view. """ def draw( self, ctx: Ctx, transition: float, incoming: Responder, outgoing: Responder ) -> None: ctx.save() ctx.translate(transition * 240, 0) outgoing.draw(ctx) ctx.restore() ctx.save() ctx.translate(-240 + transition * 240, 0) incoming.draw(ctx) ctx.restore() class ViewManager(Responder): """ The ViewManager implements stateful routing between Views. It manages a history of Views, to which new Views can be pushed and then popped. """ def __init__(self, vt: ViewTransition) -> None: """ Create a new ViewManager with a default ViewTransition. """ self._incoming: Optional[View] = None self._outgoing: Optional[View] = None # Transition time. self._time_ms = 150 self._default_vt = vt self._overriden_vt: Optional[ViewTransition] = None self._transitioning = False self._transition = 0.0 self._history: List[View] = [] def think(self, ins: InputState, delta_ms: int) -> None: if self._transitioning: self._transition += (delta_ms / 1000.0) * (1000 / self._time_ms) if self._transition >= 1.0: self._transition = 0 self._transitioning = False self._outgoing = None if self._outgoing is not None: self._outgoing.think(ins, delta_ms) if self._incoming is not None: self._incoming.think(ins, delta_ms) def draw(self, ctx: Ctx) -> None: if self._transitioning: vt = self._default_vt if self._overriden_vt is not None: vt = self._overriden_vt if self._incoming is not None and self._outgoing is not None: vt.draw(ctx, self._transition, self._incoming, self._outgoing) return if self._incoming is not None: self._incoming.draw(ctx) def replace(self, r: View, overide_vt: Optional[ViewTransition] = None) -> None: """ Replace the existing view with the given View, optionally using a given ViewTransition instead of the default. The new view will _not_ be added to history! """ self._outgoing = self._incoming self._incoming = r self._incoming.on_enter() self._overriden_vt = overide_vt if self._outgoing is None: return self._transitioning = True self._transition = 0.0 def push(self, r: View, override_vt: Optional[ViewTransition] = None) -> None: """ Push a View to the history stack and start transitioning to it. If set, override_vt will be used instead of the default ViewTransition animation. """ if self._incoming is not None: self._history.append(self._incoming) self.replace(r, override_vt) def pop(self, override_vt: Optional[ViewTransition] = None) -> None: """ Pop a view from the history stack and start transitioning to it. If set, override_vt will be used instead of the default ViewTransition animation. """ r = self._history.pop() self.replace(r, override_vt)