|
|
|
@ -89,6 +89,74 @@ def backlight_fade(val: int, delay: int = 14000, step: int = 15) -> None:
|
|
|
|
|
display.backlight(val)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Component events. Should be different from `io.TOUCH_*` events.
|
|
|
|
|
# Event dispatched when components should draw to the display, if they are
|
|
|
|
|
# marked for re-paint.
|
|
|
|
|
RENDER = const(-255)
|
|
|
|
|
# Event dispatched when components should mark themselves for re-painting.
|
|
|
|
|
REPAINT = const(-256)
|
|
|
|
|
|
|
|
|
|
# How long, in milliseconds, should the layout rendering task sleep between
|
|
|
|
|
# the render calls.
|
|
|
|
|
_RENDER_DELAY_MS = const(10)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Component:
|
|
|
|
|
"""
|
|
|
|
|
Abstract class.
|
|
|
|
|
|
|
|
|
|
Components are GUI classes that inherit `Component` and form a tree, with a
|
|
|
|
|
`Layout` at the root, and other components underneath. Components that
|
|
|
|
|
have children, and therefore need to dispatch events to them, usually
|
|
|
|
|
override the `dispatch` method. Leaf components usually override the event
|
|
|
|
|
methods (`on_*`). Components signal a completion to the layout by raising
|
|
|
|
|
an instance of `Result`.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
|
self.repaint = True
|
|
|
|
|
|
|
|
|
|
def dispatch(self, event: int, x: int, y: int) -> None:
|
|
|
|
|
if event is RENDER:
|
|
|
|
|
self.on_render()
|
|
|
|
|
elif utils.USE_BUTTON and event is io.BUTTON_PRESSED:
|
|
|
|
|
self.on_button_pressed(x)
|
|
|
|
|
elif utils.USE_BUTTON and event is io.BUTTON_RELEASED:
|
|
|
|
|
self.on_button_released(x)
|
|
|
|
|
elif utils.USE_TOUCH and event is io.TOUCH_START:
|
|
|
|
|
self.on_touch_start(x, y)
|
|
|
|
|
elif utils.USE_TOUCH and event is io.TOUCH_MOVE:
|
|
|
|
|
self.on_touch_move(x, y)
|
|
|
|
|
elif utils.USE_TOUCH and event is io.TOUCH_END:
|
|
|
|
|
self.on_touch_end(x, y)
|
|
|
|
|
elif event is REPAINT:
|
|
|
|
|
self.repaint = True
|
|
|
|
|
|
|
|
|
|
def on_touch_start(self, x: int, y: int) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def on_touch_move(self, x: int, y: int) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def on_touch_end(self, x: int, y: int) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def on_button_pressed(self, button_number: int) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def on_button_released(self, button_number: int) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def on_render(self) -> None:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
if __debug__:
|
|
|
|
|
|
|
|
|
|
def read_content_into(self, content_store: list[str]) -> None:
|
|
|
|
|
content_store.clear()
|
|
|
|
|
content_store.append(self.__class__.__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Result(Exception):
|
|
|
|
|
"""
|
|
|
|
|
When components want to trigger layout completion, they do so through
|
|
|
|
@ -168,13 +236,70 @@ class Layout:
|
|
|
|
|
returns, the others are closed and `create_tasks` is called again.
|
|
|
|
|
|
|
|
|
|
Usually overridden to add another tasks to the list."""
|
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
|
|
if __debug__:
|
|
|
|
|
|
|
|
|
|
def read_content_into(self, content_store: list[str]) -> None:
|
|
|
|
|
content_store.clear()
|
|
|
|
|
content_store.append(self.__class__.__name__)
|
|
|
|
|
tasks = (self.handle_rendering(),)
|
|
|
|
|
if utils.USE_BUTTON:
|
|
|
|
|
tasks = tasks + (self.handle_button(),)
|
|
|
|
|
if utils.USE_TOUCH:
|
|
|
|
|
tasks = tasks + (self.handle_touch(),)
|
|
|
|
|
return tasks
|
|
|
|
|
|
|
|
|
|
def handle_touch(self) -> Generator:
|
|
|
|
|
"""Task that is waiting for the user input."""
|
|
|
|
|
touch = loop.wait(io.TOUCH)
|
|
|
|
|
while True:
|
|
|
|
|
# Using `yield` instead of `await` to avoid allocations.
|
|
|
|
|
event, x, y = yield touch
|
|
|
|
|
workflow.idle_timer.touch()
|
|
|
|
|
self.dispatch(event, x, y)
|
|
|
|
|
# We dispatch a render event right after the touch. Quick and dirty
|
|
|
|
|
# way to get the lowest input-to-render latency.
|
|
|
|
|
self.dispatch(RENDER, 0, 0)
|
|
|
|
|
|
|
|
|
|
def handle_button(self) -> Generator:
|
|
|
|
|
"""Task that is waiting for the user input."""
|
|
|
|
|
button = loop.wait(io.BUTTON)
|
|
|
|
|
while True:
|
|
|
|
|
event, button_num = yield button
|
|
|
|
|
workflow.idle_timer.touch()
|
|
|
|
|
self.dispatch(event, button_num, 0)
|
|
|
|
|
self.dispatch(RENDER, 0, 0)
|
|
|
|
|
|
|
|
|
|
def _before_render(self) -> None:
|
|
|
|
|
# Before the first render, we dim the display.
|
|
|
|
|
backlight_fade(style.BACKLIGHT_NONE)
|
|
|
|
|
# Clear the screen of any leftovers, make sure everything is marked for
|
|
|
|
|
# repaint (we can be running the same layout instance multiple times)
|
|
|
|
|
# and paint it.
|
|
|
|
|
display.clear()
|
|
|
|
|
self.dispatch(REPAINT, 0, 0)
|
|
|
|
|
self.dispatch(RENDER, 0, 0)
|
|
|
|
|
|
|
|
|
|
if __debug__ and self.should_notify_layout_change:
|
|
|
|
|
from apps.debug import notify_layout_change
|
|
|
|
|
|
|
|
|
|
# notify about change and do not notify again until next await.
|
|
|
|
|
# (handle_rendering might be called multiple times in a single await,
|
|
|
|
|
# because of the endless loop in __iter__)
|
|
|
|
|
self.should_notify_layout_change = False
|
|
|
|
|
notify_layout_change(self)
|
|
|
|
|
|
|
|
|
|
# Display is usually refreshed after every loop step, but here we are
|
|
|
|
|
# rendering everything synchronously, so refresh it manually and turn
|
|
|
|
|
# the brightness on again.
|
|
|
|
|
refresh()
|
|
|
|
|
backlight_fade(self.BACKLIGHT_LEVEL)
|
|
|
|
|
|
|
|
|
|
def handle_rendering(self) -> loop.Task: # type: ignore [awaitable-is-generator]
|
|
|
|
|
"""Task that is rendering the layout in a busy loop."""
|
|
|
|
|
self._before_render()
|
|
|
|
|
sleep = self.RENDER_SLEEP
|
|
|
|
|
while True:
|
|
|
|
|
# Wait for a couple of ms and render the layout again. Because
|
|
|
|
|
# components use re-paint marking, they do not really draw on the
|
|
|
|
|
# display needlessly. Using `yield` instead of `await` to avoid allocations.
|
|
|
|
|
# TODO: remove the busy loop
|
|
|
|
|
yield sleep
|
|
|
|
|
self.dispatch(RENDER, 0, 0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def wait_until_layout_is_running() -> Awaitable[None]: # type: ignore [awaitable-is-generator]
|
|
|
|
|