st3m.ui.view module

Warning

In earlier versions of flow3r there was no clear distinction between View and st3m.ui.Application. This was inconvenient so we are slowly transitioning to a new model where an application may use views while being a distinct entity. This transition is incomplete as we tried to wrap up the release for Chaos Communication Congress 2024 and many new features rely on the new model.

This documentation is the old view documentation. We did not rewrite it for this release yet as it is unclear how to use it future safely. Features like .on_enter() clash with their st3m.ui.application counterpart, which will be rectified soon by introducing new methods to Application. Right now it is difficult to use them for both purposes.

We will try to keep existing View/ViewManager applications running and will provide a clean way for new applications to do so soon, but in the meantime please don’t try to get hacky with these APIs because cannot maintain every edge case.

Managing multiple views

If you want to write a more advanced application you probably also want to display more than one screen (or view as we call them). With just the Responder class this can become a bit tricky as it never knows when it is visible and when it is not. It also doesn’t directly allow you to launch a new screen.

To help you with that you can use a View instead. It can tell you when it becomes visible, when it is about to become inactive (invisible) and you can also use it to bring a new screen or widget into the foreground or remove it again from the screen.

Example 1: Managing two views

In this example we use a basic View to switch between to different screens using a button. One screen shows a red square, the other one a green square. You can of course put any kind of complex processing into the two different views. We make use of an InputController again to handle the button presses.

from st3m.input import InputController
from st3m.ui.view import View
import st3m.run

class SecondScreen(View):
    def __init__(self) -> None:
        self.input = InputController()
        self._vm = None

    def on_enter(self, vm: Optional[ViewManager]) -> None:
        self._vm = vm

        # Ignore the button which brought us here until it is released
        self.input._ignore_pressed()

    def draw(self, ctx: Context) -> None:
        # Paint the background black
        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
        # Green square
        ctx.rgb(0, 1, 0).rectangle(-20, -20, 40, 40).fill()

    def think(self, ins: InputState, delta_ms: int) -> None:
        self.input.think(ins, delta_ms) # let the input controller to its magic

        # No need to handle returning back to Example on button press - the
        # flow3r's ViewManager takes care of that automatically.


class Example(View):
    def __init__(self) -> None:
        self.input = InputController()
        self._vm = None

    def draw(self, ctx: Context) -> None:
        # Paint the background black
        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
        # Red square
        ctx.rgb(1, 0, 0).rectangle(-20, -20, 40, 40).fill()


    def on_enter(self, vm: Optional[ViewManager]) -> None:
        self._vm = vm
        self.input._ignore_pressed()

    def think(self, ins: InputState, delta_ms: int) -> None:
        self.input.think(ins, delta_ms) # let the input controller to its magic

        if self.input.buttons.app.middle.pressed:
            self._vm.push(SecondScreen())

st3m.run.run_view(Example())

Try it using mpremote. The OS shoulder button (right shoulder unless swapped in settings) switches between the two views. To avoid that the still pressed button immediately closes SecondScreen we make us of a special method of the InputController which hides the pressed button from the view until it is released again.

Note

Pressing the OS shoulder button in REPL mode will currently reset the badge.

Until this is fixed, you can test view switching by copying the app to your badge and running from the menu.

Example 2: Easier view management

The above code is so universal that we provide a special view which takes care of this boilerplate: BaseView. It integrated a local InputController on self.input and a copy of the ViewManager which caused the View to enter on self.vm.

Here is our previous example rewritten to make use of BaseView:

from st3m.ui.view import BaseView
import st3m.run

class SecondScreen(BaseView):
    def __init__(self) -> None:
        # Remember to call super().__init__() if you implement your own
        # constructor!
        super().__init__()

    def on_enter(self, vm: Optional[ViewManager]) -> None:
        # Remember to call super().on_enter() if you implement your own
        # on_enter!
        super().on_enter(vm)

    def draw(self, ctx: Context) -> None:
        # Paint the background black
        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
        # Green square
        ctx.rgb(0, 1, 0).rectangle(-20, -20, 40, 40).fill()

class Example(BaseView):
    def draw(self, ctx: Context) -> None:
        # Paint the background black
        ctx.rgb(0, 0, 0).rectangle(-120, -120, 240, 240).fill()
        # Red square
        ctx.rgb(1, 0, 0).rectangle(-20, -20, 40, 40).fill()

    def think(self, ins: InputState, delta_ms: int) -> None:
        super().think(ins, delta_ms) # Let BaseView do its thing

        if self.input.buttons.app.middle.pressed:
            self.vm.push(SecondScreen())

st3m.run.run_view(Example())

In some cases, it’s useful to know more about the view’s lifecycle, so tasks like long-blocking loading screens can be synchronized with view transition animations. You’ll also be interested in those events when doing unusual things with rendering, such as partial redraws or direct framebuffer manipulation.

Here’s the list of methods that you can implement in your class:

Function

Meaning

.on_enter(vm)

The view has became active and is about to start receiving .think() and .draw() calls.

.on_enter_done()

The view transition has finished animating; the whole screen is now under the control of the view.

If you want to perform a long running blocking task (such as loading audio samples), it’s a good idea to initiate it here to not block during the transition animation.

.on_exit()

The view has became inactive and started to transition out; no further .think() calls will be made until the next .on_enter() unless you return True from this handler.

Do the clean-up of shared resources (such as LEDs or the media engine) here. This way you won’t step onto the next view’s shoes, as it may want to use them in its .on_enter() already.

Note: When returning True, make sure not to handle input events not meant for this view in your .think() handler during transition animation (see .is_active() below).

.on_exit_done()

The view transition has finished animating; no further .draw() or .think() calls will be made until the next .on_enter().

A good place to do the clean-up of resources you have exclusive control over, such as bl00mbox channels.

.show_icons()

Return True from it to have indicator icons drawn on top of your view by the system, just like in the main menu. Defaults to False.

ViewManager also provides some methods to make handling common cases easier:

Function / property

Meaning

.is_active(view)

Returns a bool indicating whether the passed view is currently active. The active view is the one that’s expected to be in control of user’s input and the view stack. A view becomes active at .on_enter() and stops being active at .on_exit().

.transitioning

Whether a transition animation is currently in progress.

.direction

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.

On top of that, BaseView implements an additional helper method .is_active(), which is simply a bit less awkward way to call self.vm.is_active(self).