diff --git a/python_payload/st3m/about.py b/python_payload/st3m/about.py new file mode 100644 index 0000000000000000000000000000000000000000..3f0b03bbe0931db07a7da44db7a0dbe4ebf8d69d --- /dev/null +++ b/python_payload/st3m/about.py @@ -0,0 +1,227 @@ +from ctx import Context +from st3m import InputState, Responder +from st3m.ui.view import BaseView, ViewTransitionSwipeRight +from st3m.utils import tau +from st3m.goose import List, Optional +import math + + +class Screen(Responder): + """ + A screen with some text. Just the text part, without background. + """ + + START_Y = -50 + FONT = "Camp Font 3" + FONT_SIZE = 15 + + def __init__(self, text: List[str]) -> None: + self.text = text + + def draw(self, ctx: Context) -> None: + ctx.font = self.FONT + ctx.font_size = self.FONT_SIZE + + y = self.START_Y + for line in self.text: + ctx.move_to(0, y) + ctx.text(line) + y += self.FONT_SIZE + + def think(self, ins: InputState, delta_ms: int) -> None: + pass + + +class HeroScreen(Screen): + """ + A screen with large text in the middle. + """ + + START_Y = 0 + FONT = "Camp Font 2" + FONT_SIZE = 30 + + +class IntroScreen(Screen): + """ + Title card which shows on About app startup. + """ + + START_Y = 25 + + def __init__(self) -> None: + super().__init__(["Chaos", "Communication", "Camp 2023"]) + + def draw(self, ctx: Context) -> None: + ctx.text_align = ctx.MIDDLE + + ctx.font = "Camp Font 2" + ctx.font_size = 50 + + ctx.move_to(0, -33) + ctx.text("flow3r") + ctx.move_to(0, -3) + ctx.text("badge") + + super().draw(ctx) + + +class About(BaseView): + """ + A pseudo-app (applet?) which displays an about screen for the badge. + """ + + def __init__(self) -> None: + self.ts = 0.0 + self.screens: List[Screen] = [ + IntroScreen(), + HeroScreen( + [ + "flow3r.garden", + ] + ), + Screen( + [ + "This is Free", + "Software based on", + "code by numerous", + "third parties.", + "", + "Visit", + "flow3r.garden/license", + "for more info.", + ] + ), + ] + self.screen_ix = 0 + self.screen_ix_anim = 0.0 + super().__init__() + + def think(self, ins: InputState, delta_ms: int) -> None: + super().think(ins, delta_ms) + self.ts += delta_ms / 1000 + + if self.input.left_shoulder.middle.pressed: + if self.vm is not None: + self.vm.pop(ViewTransitionSwipeRight()) + + # Change target screen intent. + if self.input.left_shoulder.left.pressed and self._can_left(): + self.screen_ix -= 1 + if self.input.left_shoulder.right.pressed and self._can_right(): + self.screen_ix += 1 + + # Calculate animation/transitions. + diff = self.screen_ix - self.screen_ix_anim + if abs(diff) < 0.01: + self.screen_ix_anim = self.screen_ix + else: + if diff > 0: + diff = min(diff, 10 * (delta_ms / 1000)) + else: + diff = max(diff, -10 * (delta_ms / 1000)) + self.screen_ix_anim += diff + + def _can_left(self, ix: Optional[int] = None) -> bool: + """ + Returns true if the given screen index (or the current screen index) can + be decreased. + """ + if ix is None: + ix = self.screen_ix + return ix > 0 + + def _can_right(self, ix: Optional[int] = None) -> bool: + """ + Returns true if the given screen index (or the current screen index) can + be increased. + """ + if ix is None: + ix = self.screen_ix + return ix < len(self.screens) - 1 + + def draw(self, ctx: Context) -> None: + # Background + ctx.rectangle(-120, -120, 240, 240) + ctx.rgb(117 / 255, 255 / 255, 226 / 255) + ctx.fill() + + ctx.arc(0, 350, 300, 0, tau, 0) + ctx.rgb(61 / 255, 165 / 255, 30 / 255) + ctx.fill() + + ctx.save() + y = -40 + math.cos(self.ts * 2 + 10) * 10 + ctx.translate(0, y) + for i in range(5): + a = i * tau / 5 + self.ts + size = 30 + math.cos(self.ts) * 5 + ctx.save() + ctx.rotate(a) + ctx.translate(30, 0) + ctx.arc(0, 0, size, 0, tau, 0) + ctx.gray(1) + ctx.fill() + ctx.restore() + + ctx.arc(0, 0, 20, 0, tau, 0) + ctx.rgb(255 / 255, 247 / 255, 46 / 255) + ctx.fill() + ctx.restore() + + # Prepare list of screens to draw. We draw at most two screens if we're + # in a transition, otherwies just one. + # + # List of (screen index, screen object, draw offset) tuples. + draw = [] + + diff = self.screen_ix - self.screen_ix_anim + if diff > 0.01 or diff < -0.01: + ix = math.floor(self.screen_ix_anim) + draw = [ + (ix, self.screens[ix], self.screen_ix_anim - ix), + (ix + 1, self.screens[ix + 1], (self.screen_ix_anim - ix) - 1), + ] + else: + draw = [ + (self.screen_ix, self.screens[self.screen_ix], 0), + ] + + # Draw currently visible screens. + for ix, screen, offs in draw: + ctx.save() + ctx.translate(-240 * offs, 0) + + # Opaque circle + ctx.start_group() + ctx.global_alpha = 0.5 + ctx.rgb(0, 0, 0) + ctx.arc(0, 0, 90, 0, tau, 0) + ctx.fill() + + # Draw arrows. + if self._can_left(ix): + ctx.move_to(-105, 20) + ctx.font = "Material Icons" + ctx.font_size = 30 + ctx.text_align = ctx.MIDDLE + ctx.text("\ue5c4") + if self._can_right(ix): + ctx.move_to(105, 20) + ctx.font = "Material Icons" + ctx.font_size = 30 + ctx.text_align = ctx.MIDDLE + ctx.text("\ue5c8") + ctx.end_group() + + # Text + ctx.start_group() + ctx.global_alpha = 1.0 + ctx.gray(1) + + ctx.text_align = ctx.MIDDLE + + screen.draw(ctx) + + ctx.end_group() + ctx.restore() diff --git a/python_payload/st3m/run.py b/python_payload/st3m/run.py index df94f95d1b2cd1401cc1d539ac9793ee0f0619b6..d3daec0e995e6f1f0ab5158164146c16544cf3db 100644 --- a/python_payload/st3m/run.py +++ b/python_payload/st3m/run.py @@ -6,11 +6,13 @@ from st3m.ui.menu import ( MenuItemForeground, MenuItemNoop, MenuItemAction, + MenuItemLaunchPersistentView, ) from st3m.ui.elements import overlays from st3m.ui.view import View, ViewManager, ViewTransitionBlend from st3m.ui.elements.menus import SimpleMenu, SunMenu from st3m.application import discover_bundles, BundleMetadata +from st3m.about import About from st3m import settings, logging, processors import captouch, audio, leds, gc @@ -124,7 +126,7 @@ def run_main() -> None: MenuItemBack(), MenuItemForeground("Settings", menu_settings), MenuItemNoop("Disk Mode"), - MenuItemNoop("About"), + MenuItemLaunchPersistentView("About", About), MenuItemAction("Yeet Local Changes", yeet_local_changes), MenuItemAction("Reboot", lambda: machine.reset()), ], diff --git a/python_payload/st3m/ui/menu.py b/python_payload/st3m/ui/menu.py index 70e68c142b331a09324facc76bd253f0f7a34917..36ec747e2c4c62022a866a155df02e04b724f163 100644 --- a/python_payload/st3m/ui/menu.py +++ b/python_payload/st3m/ui/menu.py @@ -88,6 +88,30 @@ class MenuItemAction(MenuItem): return self._label +class MenuItemLaunchPersistentView(MenuItem): + """ + A MenuItem which lazily loads a View class on first run. + """ + + def __init__(self, label: str, cons: Callable[[], View]) -> None: + self._label = label + self._cons = cons + self._instance: Optional[View] = None + + def press(self, vm: Optional[ViewManager]) -> None: + if self._instance is None: + self._instance = self._cons() + if vm is not None: + vm.push(self._instance, ViewTransitionSwipeLeft()) + else: + log.warning( + f"Could not foreground {self._instance} as no ViewManager is present" + ) + + def label(self) -> str: + return self._label + + class MenuItemNoop(MenuItem): """ A MenuItem which does nothing.