from st3m.reactor import Responder from st3m.goose import ABCBase, abstractmethod, Optional, List, Enum from st3m.input import InputState, InputController from ctx import Context import machine import utime 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 BaseView class. """ def on_enter(self, vm: Optional["ViewManager"]) -> None: """ Called when the View has just become active. """ pass def on_exit(self) -> bool: """ Called when the View is about to become inactive. If it returns True, think calls will continue to happen until on_exit_done. """ return False def on_enter_done(self) -> None: """ Called after a transition into the view has finished. """ pass def on_exit_done(self) -> None: """ Called after a transition out of the view has finished. """ pass def show_icons(self) -> bool: """ View should return True if it accepts having system icons drawn on top if it. """ return False class BaseView(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().__init__() in __init__() and super().think() in think()! """ __slots__ = ("input", "vm") def __init__(self) -> None: self.input = InputController() self.vm: Optional["ViewManager"] = None def on_enter(self, vm: Optional["ViewManager"]) -> None: self.input._ignore_pressed() self.vm = vm def think(self, ins: InputState, delta_ms: int) -> None: self.input.think(ins, delta_ms) def is_active(self) -> bool: if not self.vm: return False return self.vm.is_active(self) 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: Context, 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: Context, transition: float, incoming: Responder, outgoing: Responder ) -> None: ctx.save() outgoing.draw(ctx) ctx.restore() ctx.save() ctx.global_alpha = transition incoming.draw(ctx) ctx.restore() class ViewTransitionSwipeLeft(ViewTransition): """ Swipe the outoing view to the left and replace it with the incoming view. """ def draw( self, ctx: Context, transition: float, incoming: Responder, outgoing: Responder ) -> None: ctx.save() ctx.translate(int(transition * -240), 0) outgoing.draw(ctx) ctx.restore() ctx.save() ctx.translate(240 + int(transition * -240), 0) ctx.rectangle(-120, -120, 240, 240) ctx.clip() 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: Context, transition: float, incoming: Responder, outgoing: Responder ) -> None: ctx.save() ctx.translate(int(transition * 240), 0) outgoing.draw(ctx) ctx.restore() ctx.save() ctx.translate(-240 + int(transition * 240), 0) ctx.rectangle(-120, -120, 240, 240) ctx.clip() incoming.draw(ctx) ctx.restore() class ViewTransitionDirection(Enum): NONE = 1 FORWARD = 2 BACKWARD = 3 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, debug: bool) -> None: """ Create a new ViewManager with a default ViewTransition. """ self._incoming: Optional[View] = None self._outgoing: Optional[View] = None self._pending: Optional[View] = None self._pending_vt: Optional[ViewTransition] = None self._pending_direction: Optional[ViewTransitionDirection] = None self._debug = debug # 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] = [] self._input = InputController() self._first_think = False self._fully_drawn = 0 self._outgoing_wants_to_think = False def _end_transition(self) -> None: if not self._transitioning: return self._transitioning = False if self._outgoing is not None: self._outgoing.on_exit_done() self._outgoing = None if self._incoming is not None: self._incoming.on_enter_done() def _perform_pending(self): if not self._pending: return self._end_transition() self._transitioning = True self._transition = 0.0 self._first_think = True self._fully_drawn = 0 self._direction = self._pending_direction self._overriden_vt = self._pending_vt self._outgoing = self._incoming self._incoming = self._pending self._pending = None if self._outgoing is not None: self._outgoing_wants_to_think = self._outgoing.on_exit() self._incoming.on_enter(self) if self._outgoing is None: self._end_transition() def think(self, ins: InputState, delta_ms: int) -> None: self._input.think(ins, delta_ms) if self._transitioning: if not self._first_think: self._transition += (delta_ms / 1000.0) * (1000 / self._time_ms) else: self._first_think = False if self._transition >= 1.0: self._transition = 1.0 if self._fully_drawn > 1: self._end_transition() if self._input.buttons.os.middle.pressed: if not self._history and self._debug: utime.sleep(0.5) machine.reset() else: self.pop(ViewTransitionSwipeRight()) if self._outgoing is not None and self._outgoing_wants_to_think: self._outgoing.think(ins, delta_ms) if self._incoming is not None: self._incoming.think(ins, delta_ms) self._perform_pending() def draw(self, ctx: Context) -> None: if self._transitioning: if self._transition == 0.0: ctx.save() self._outgoing.draw(ctx) ctx.restore() return if self._transition >= 1.0: self._fully_drawn += 1 ctx.save() self._incoming.draw(ctx) ctx.restore() return 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: ctx.save() vt.draw(ctx, self._transition, self._incoming, self._outgoing) ctx.restore() return if self._incoming is not None: ctx.save() self._incoming.draw(ctx) ctx.restore() def replace( self, r: View, override_vt: Optional[ViewTransition] = None, direction: ViewTransitionDirection = ViewTransitionDirection.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._pending = r self._pending_vt = override_vt self._pending_direction = direction 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._pending is not None: self._history.append(self._pending) elif self._incoming is not None: self._history.append(self._incoming) self.replace(r, override_vt, ViewTransitionDirection.FORWARD) def pop(self, override_vt: Optional[ViewTransition] = None, depth: int = 1) -> None: """ Pop a view (or more views) from the history stack and start transitioning. If set, override_vt will be used instead of the default ViewTransition animation. """ r = None for i in range(depth): if len(self._history) < 1: break r = self._history.pop() if r: self.replace(r, override_vt, ViewTransitionDirection.BACKWARD) def wants_icons(self) -> bool: """ Returns true if the current active view wants icon to be drawn on top of it. """ if self._incoming is not None: return self._incoming.show_icons() else: return True def is_active(self, view: View) -> bool: """ Returns true if the passed view is currently the active one. """ return (self._incoming == view and not self._pending) or self._pending == view @property def transitioning(self) -> bool: """ Returns true if a transition is in progress. """ return self._transitioning or self._pending @property def direction(self) -> ViewTransitionDirection: """ Returns the direction in which the currently active view has became one: - ViewTransitionDirection.NONE if it has replaced another view - ViewTransitionDirection.FORWARD if it was pushed into the stack - ViewTransitionDirection.BACKWARD if another view was popped """ return self._direction