From 1bd0f60fbee2b575a64f4e5931b83e8833f56659 Mon Sep 17 00:00:00 2001
From: moon2 <moon2protonmail@protonmail.com>
Date: Thu, 2 May 2024 15:49:12 +0200
Subject: [PATCH] status menu: help viewer

This adds the .get_help() method to views.
application writers can use this to return
context-dependent help strings to be displayed
by the system menu.

In the future this API could also be used to
return more complex objects but this makes
for a good start :D
---
 python_payload/apps/led_painter/__init__.py | 12 +++++
 python_payload/st3m/ui/elements/overlays.py | 11 ++++-
 python_payload/st3m/ui/help.py              | 55 +++++++++++++++++++++
 python_payload/st3m/ui/view.py              | 14 ++++++
 python_payload/st3m/utils.py                | 46 +++++++++++++++++
 5 files changed, 136 insertions(+), 2 deletions(-)
 create mode 100644 python_payload/st3m/ui/help.py

diff --git a/python_payload/apps/led_painter/__init__.py b/python_payload/apps/led_painter/__init__.py
index 3c55c2b375..09ca2b03ab 100644
--- a/python_payload/apps/led_painter/__init__.py
+++ b/python_payload/apps/led_painter/__init__.py
@@ -26,6 +26,18 @@ log.info("hello led painter")
 
 
 class LEDPainter(Application):
+    def get_help(self):
+        help_text = (
+            "use petals 0 to 5 to set rgb values or petals "
+            "6 or 7 for shortcuts to black and white.\n\n"
+            "use the app button to move cw/ccw through "
+            "the LEDs by pressing left/right and enable "
+            "or disable drawing to LEDs by pressing down.\n\n"
+            "your pattern is saved when you exit and can be "
+            'set as a "LED wallpaper" in settings->appearance.'
+        )
+        return help_text
+
     def __init__(self, app_ctx: ApplicationContext) -> None:
         super().__init__(app_ctx)
         self.input = InputController()
diff --git a/python_payload/st3m/ui/elements/overlays.py b/python_payload/st3m/ui/elements/overlays.py
index d9ce9960ae..264a8a8784 100644
--- a/python_payload/st3m/ui/elements/overlays.py
+++ b/python_payload/st3m/ui/elements/overlays.py
@@ -15,6 +15,7 @@ import st3m.power
 from ctx import Context
 import st3m.wifi
 from st3m.application import viewmanager_is_in_application
+from st3m.ui.help import Help
 
 import math
 import audio
@@ -147,6 +148,9 @@ class Compositor(Responder):
         self._volume = OverlayVolume(self.input)
         self.add_overlay(self._volume)
 
+    def get_help(self):
+        return self.main.get_help()
+
     def _enabled_overlays(self) -> List[Responder]:
         res: List[Responder] = []
         for kind in _all_kinds:
@@ -380,18 +384,19 @@ class OverlaySystemMenu(Overlay):
         self.exit_app = methodprovider.exit_app
         self.close_menu = methodprovider.close_system_menu
         self.go_home = methodprovider.go_home
+        self.get_help = methodprovider.get_help
         self._menu_pos = 0
         self._os_menu_entries = [
             "resume",
             "go home",
+            "help",
             # "mixer",
-            # "help",
         ]
         self._app_menu_entries = [
             "resume",
             "exit app",
+            "help",
             # "mixer",
-            # "help",
         ]
         self.active = False
         self.sub = None
@@ -422,6 +427,8 @@ class OverlaySystemMenu(Overlay):
         if self.input.buttons.app.middle.released:
             if self._menu_pos == 0:
                 self.close_menu()
+            elif self._menu_pos == 2:
+                self.sub = Help(self.input, self.get_help())
             elif self.in_app:
                 if self._menu_pos == 1:
                     self.exit_app()
diff --git a/python_payload/st3m/ui/help.py b/python_payload/st3m/ui/help.py
new file mode 100644
index 0000000000..a0c8824009
--- /dev/null
+++ b/python_payload/st3m/ui/help.py
@@ -0,0 +1,55 @@
+from st3m import Responder
+from st3m.utils import wrap_text
+
+
+class Help(Responder):
+    def __init__(self, inputcontroller, help_text):
+        self.input = inputcontroller
+        self.help_text = help_text
+        self.override_os_button_back = False
+        self.line_height = 16
+        self.line_width = 190
+        self.line_height_steps = 3
+        self.x = -self.line_width / 2
+        self.y = -80
+        self.lines = None
+
+    def think(self, ins, delta_ms):
+        if self.lines is None:
+            return
+        ud_dir = self.input.buttons.app.right.pressed
+        ud_dir -= self.input.buttons.app.left.pressed
+        self.y += -ud_dir * self.line_height * self.line_height_steps
+        if self.y > -80:
+            self.y = -80
+        min_y = -80 - self.line_height * max(len(self.lines) - 8, 0)
+        if self.y < min_y:
+            self.y = min_y
+
+    def draw(self, ctx):
+        ctx.rgb(0, 0, 0)
+        ctx.rectangle(-120, -120, 240, 240).fill()
+        ctx.rgb(0x81 / 255, 0xCD / 255, 0xC6 / 255)
+        ctx.move_to(0, self.y)
+        ctx.text_align = ctx.CENTER
+        if not (self.y < -120):
+            ctx.font_size = 24
+            ctx.text("~ help ~")
+        ctx.font_size = 16
+        offset = self.line_height
+        if not isinstance(self.help_text, str):
+            offset += self.line_height
+            ctx.move_to(0, self.y + offset)
+            ctx.text("no help found :/")
+            return
+        ctx.text_align = ctx.LEFT
+        if self.lines is None:
+            self.lines = wrap_text(self.help_text, self.line_width, ctx)
+        for line in self.lines:
+            offset += self.line_height
+            if (self.y + offset) < (-120):
+                continue
+            elif (self.y + offset) > (120 + self.line_height):
+                break
+            ctx.move_to(self.x, self.y + offset)
+            ctx.text(line)
diff --git a/python_payload/st3m/ui/view.py b/python_payload/st3m/ui/view.py
index 0247cb8c58..83c52afa92 100644
--- a/python_payload/st3m/ui/view.py
+++ b/python_payload/st3m/ui/view.py
@@ -51,6 +51,15 @@ class View(Responder):
         """
         return False
 
+    def get_help(self) -> Optional[str]:
+        """
+        Returns help string to be displayed by the system menu. May be
+        dynamic/context-dependent. May in the future be also used
+        to return more complex helpy objects, so ideally check the
+        output with isinstance(ret, str).
+        """
+        return None
+
 
 class BaseView(View):
     """
@@ -262,6 +271,11 @@ class ViewManager(Responder):
             return False
         return self._incoming.override_os_button_back
 
+    def get_help(self):
+        if self._incoming is None:
+            return None
+        return self._incoming.get_help()
+
     def exit_view(self):
         if not self._history and self._debug:
             utime.sleep(0.5)
diff --git a/python_payload/st3m/utils.py b/python_payload/st3m/utils.py
index f16295eb23..4e350a546a 100644
--- a/python_payload/st3m/utils.py
+++ b/python_payload/st3m/utils.py
@@ -152,3 +152,49 @@ def is_simulator() -> bool:
 
 
 tau = math.pi * 2
+
+
+def wrap_text(text, line_width, ctx=None):
+    """
+    wraps text to stay below a certain line width and returns
+    the wrapped lines as a list without newline characters.
+
+    if no ctx is passed the line width is interpreted as number
+    of characters, else it is the rasterized width in pixels.
+    the latter mode uses ctx.text_width() to determine width so
+    do set your ctx up in the way you'll render the result before
+    calling this.
+
+    may infinite-loop if not a single character fits the line
+    or similar edge cases. we didn't spend a lot of time on
+    this.
+    """
+    wrapped_lines = []
+    if ctx is None:
+        len_fun = len
+    else:
+        len_fun = ctx.text_width
+    for line in text.split("\n"):
+        if len_fun(line) <= line_width:
+            wrapped_lines += [line]
+        else:
+            line_words = line.split(" ")
+            subline_words = []
+            while line_words:
+                subline_words.append(line_words.pop(0))
+                if len_fun(" ".join(subline_words)) > line_width:
+                    if len(subline_words) == 1:
+                        split_word = subline_words[0]
+                        for x in range(len(split_word)):
+                            if len_fun(split_word[:x]) > line_width:
+                                break
+                        x -= 1
+                        subline_words[0] = split_word[:x]
+                        line_words.insert(0, split_word[x:])
+                    else:
+                        line_words.insert(0, subline_words.pop())
+                    wrapped_lines.append(" ".join(subline_words))
+                    subline_words = []
+            if subline_words:
+                wrapped_lines.append(" ".join(subline_words))
+    return wrapped_lines
-- 
GitLab