From 064d6d00b08f5a73f0ba2124e2c2e9f66329487a Mon Sep 17 00:00:00 2001 From: Hugh Rawlinson Date: Sat, 4 May 2024 19:02:52 +0200 Subject: [PATCH 1/4] Add menu widget --- modules/app_components/menu.py | 77 +++++++++++++++++++++++++++++++++ modules/apps/menu_demo.py | 79 ++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 modules/app_components/menu.py create mode 100644 modules/apps/menu_demo.py diff --git a/modules/app_components/menu.py b/modules/app_components/menu.py new file mode 100644 index 0000000..1de03e2 --- /dev/null +++ b/modules/app_components/menu.py @@ -0,0 +1,77 @@ +from typing import Callable, Literal, Union + +from app import App +from events.input import ButtonDownEvent +from system.eventbus import eventbus + + +class Menu: + def __init__( + self, + app: App, + menu_items: list[str] = [], + position=0, + select_handler: Union[Callable, None] = None, + back_handler: Union[Callable, None] = None, + animation_time_ms=300, + ): + self.menu_items = menu_items + self.position = position + self.select_handler = select_handler + self.back_handler = back_handler + self.animation_time_ms = animation_time_ms + self.is_animating: Literal["up", "down", "none"] = "none" + self.app = app + + eventbus.on(ButtonDownEvent, self._handle_buttondown, app) + + def _cleanup(self): + eventbus.remove(ButtonDownEvent, self._handle_buttondown, self.app) + + def _handle_buttondown(self, event: ButtonDownEvent): + match event.button: + case 0: + self.up_handler() + case 3: + self.down_handler() + case 5: + if self.back_handler is not None: + self.back_handler() + case 1: + if self.select_handler is not None: + self.select_handler( + self.menu_items[self.position % len(self.menu_items)] + ) + + def up_handler(self): + self.position = (self.position - 1) % len(self.menu_items) + + def down_handler(self): + self.position = (self.position + 1) % len(self.menu_items) + + def draw(self, ctx): + # Current menu item + ctx.font_size = 40 + ctx.text_align = ctx.CENTER + ctx.text_baseline = ctx.MIDDLE + text_height = ctx.font_size + focused_item_margin = 20 + line_height = 30 + + ctx.rgb(1, 1, 1) + ctx.move_to(0, 0).text(self.menu_items[self.position % len(self.menu_items)]) + + # Previous menu items + ctx.font_size = 20 + for i in range(1, 4): + if (self.position - i) >= 0: + ctx.move_to(0, -focused_item_margin + -i * line_height).text( + self.menu_items[self.position - i] + ) + + # Next menu items + for i in range(1, 4): + if (self.position + i) < len(self.menu_items): + ctx.move_to(0, focused_item_margin + i * line_height).text( + self.menu_items[self.position + i] + ) diff --git a/modules/apps/menu_demo.py b/modules/apps/menu_demo.py new file mode 100644 index 0000000..c4b2da7 --- /dev/null +++ b/modules/apps/menu_demo.py @@ -0,0 +1,79 @@ +from typing import Literal + +from app import App +from app_components.menu import Menu + +main_menu_items = ["numbers", "letters", "words"] + +numbers_menu_items = ["one", "two", "three", "four", "five"] + +letters_menu_items = ["a", "b", "c", "d", "e"] + +words_menu_items = ["emfcamp", "bodgeham-on-wye", "hackers", "hexpansions", "tildagon"] + + +class MenuDemo(App): + def __init__(self): + self.current_menu = "main" + self.menu = Menu( + self, + main_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + + def select_handler(self, item): + print(item) + if item in ["numbers", "letters", "words", "main"]: + self.set_menu(item) + else: + # flash or spin the word or something + pass + + def set_menu(self, menu_name: Literal["main", "numbers", "letters", "words"]): + self.menu._cleanup() + self.current_menu = menu_name + match menu_name: + case "main": + self.menu = Menu( + self, + main_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + case "numbers": + self.menu = Menu( + self, + numbers_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + case "letters": + self.menu = Menu( + self, + letters_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + case "words": + self.menu = Menu( + self, + words_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + + def back_handler(self): + if self.current_menu == "main": + return + self.set_menu("main") + + def draw_background(self, ctx): + ctx.gray(0).rectangle(-120, -120, 240, 240).fill() + + def draw(self, ctx): + self.draw_background(ctx) + self.menu.draw(ctx) + + def update(self, delta): + pass From 44a102f62b7aeb116ab14b47632822c4ca39fe60 Mon Sep 17 00:00:00 2001 From: Hugh Rawlinson Date: Sat, 4 May 2024 21:12:17 +0200 Subject: [PATCH 2/4] Use if, not match --- modules/app_components/menu.py | 25 +++++++-------- modules/apps/menu_demo.py | 57 +++++++++++++++++----------------- 2 files changed, 40 insertions(+), 42 deletions(-) diff --git a/modules/app_components/menu.py b/modules/app_components/menu.py index 1de03e2..860542c 100644 --- a/modules/app_components/menu.py +++ b/modules/app_components/menu.py @@ -29,19 +29,18 @@ def _cleanup(self): eventbus.remove(ButtonDownEvent, self._handle_buttondown, self.app) def _handle_buttondown(self, event: ButtonDownEvent): - match event.button: - case 0: - self.up_handler() - case 3: - self.down_handler() - case 5: - if self.back_handler is not None: - self.back_handler() - case 1: - if self.select_handler is not None: - self.select_handler( - self.menu_items[self.position % len(self.menu_items)] - ) + if event.button == 0: + self.up_handler() + if event.button == 3: + self.down_handler() + if event.button == 5: + if self.back_handler is not None: + self.back_handler() + if event.button == 1: + if self.select_handler is not None: + self.select_handler( + self.menu_items[self.position % len(self.menu_items)] + ) def up_handler(self): self.position = (self.position - 1) % len(self.menu_items) diff --git a/modules/apps/menu_demo.py b/modules/apps/menu_demo.py index c4b2da7..76a3665 100644 --- a/modules/apps/menu_demo.py +++ b/modules/apps/menu_demo.py @@ -33,35 +33,34 @@ def select_handler(self, item): def set_menu(self, menu_name: Literal["main", "numbers", "letters", "words"]): self.menu._cleanup() self.current_menu = menu_name - match menu_name: - case "main": - self.menu = Menu( - self, - main_menu_items, - select_handler=self.select_handler, - back_handler=self.back_handler, - ) - case "numbers": - self.menu = Menu( - self, - numbers_menu_items, - select_handler=self.select_handler, - back_handler=self.back_handler, - ) - case "letters": - self.menu = Menu( - self, - letters_menu_items, - select_handler=self.select_handler, - back_handler=self.back_handler, - ) - case "words": - self.menu = Menu( - self, - words_menu_items, - select_handler=self.select_handler, - back_handler=self.back_handler, - ) + if menu_name == "main": + self.menu = Menu( + self, + main_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + elif menu_name == "numbers": + self.menu = Menu( + self, + numbers_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + elif menu_name == "letters": + self.menu = Menu( + self, + letters_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) + elif menu_name == "words": + self.menu = Menu( + self, + words_menu_items, + select_handler=self.select_handler, + back_handler=self.back_handler, + ) def back_handler(self): if self.current_menu == "main": From a1fdd29223c800b6e93af61a1e00ead10b21ebe9 Mon Sep 17 00:00:00 2001 From: Hugh Rawlinson Date: Sat, 4 May 2024 21:17:39 +0200 Subject: [PATCH 3/4] Remove unused variable --- modules/app_components/menu.py | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/app_components/menu.py b/modules/app_components/menu.py index 860542c..d5e576c 100644 --- a/modules/app_components/menu.py +++ b/modules/app_components/menu.py @@ -53,7 +53,6 @@ def draw(self, ctx): ctx.font_size = 40 ctx.text_align = ctx.CENTER ctx.text_baseline = ctx.MIDDLE - text_height = ctx.font_size focused_item_margin = 20 line_height = 30 From 1f385183f7990e182c1c20d9c5e631a420c24d5b Mon Sep 17 00:00:00 2001 From: Hugh Rawlinson Date: Mon, 6 May 2024 00:49:47 +0200 Subject: [PATCH 4/4] Add animation --- modules/app_components/menu.py | 82 +++++++++++++++++++++++++--------- modules/apps/menu_demo.py | 3 +- 2 files changed, 62 insertions(+), 23 deletions(-) diff --git a/modules/app_components/menu.py b/modules/app_components/menu.py index d5e576c..15c235c 100644 --- a/modules/app_components/menu.py +++ b/modules/app_components/menu.py @@ -1,27 +1,42 @@ -from typing import Callable, Literal, Union +from typing import Any, Callable, Literal, Union from app import App -from events.input import ButtonDownEvent +from events.input import BUTTON_TYPES, ButtonDownEvent from system.eventbus import eventbus +def ease_out_quart(x): + return 1 - pow(1 - x, 4) + + class Menu: def __init__( self, app: App, menu_items: list[str] = [], position=0, - select_handler: Union[Callable, None] = None, + select_handler: Union[Callable[[str], Any], None] = None, back_handler: Union[Callable, None] = None, - animation_time_ms=300, + speed_ms=300, + item_font_size=20, + item_line_height=30, + focused_item_font_size=40, + focused_item_margin=20, ): + self.app = app self.menu_items = menu_items self.position = position self.select_handler = select_handler self.back_handler = back_handler - self.animation_time_ms = animation_time_ms - self.is_animating: Literal["up", "down", "none"] = "none" - self.app = app + self.speed_ms = speed_ms + self.item_font_size = item_font_size + self.item_line_height = item_line_height + self.focused_item_font_size = focused_item_font_size + self.focused_item_margin = focused_item_margin + + self.animation_time_ms = 0 + # self.is_animating: Literal["up", "down", "none"] = "none" + self.is_animating: Literal["up", "down", "none"] = "up" eventbus.on(ButtonDownEvent, self._handle_buttondown, app) @@ -29,47 +44,72 @@ def _cleanup(self): eventbus.remove(ButtonDownEvent, self._handle_buttondown, self.app) def _handle_buttondown(self, event: ButtonDownEvent): - if event.button == 0: + if BUTTON_TYPES["UP"] in event.button: self.up_handler() - if event.button == 3: + if BUTTON_TYPES["DOWN"] in event.button: self.down_handler() - if event.button == 5: + if BUTTON_TYPES["CANCEL"] in event.button: if self.back_handler is not None: self.back_handler() - if event.button == 1: + if BUTTON_TYPES["CONFIRM"] in event.button: if self.select_handler is not None: self.select_handler( self.menu_items[self.position % len(self.menu_items)] ) def up_handler(self): + self.is_animating = "up" + self.animation_time_ms = 0 self.position = (self.position - 1) % len(self.menu_items) def down_handler(self): + self.is_animating = "down" + self.animation_time_ms = 0 self.position = (self.position + 1) % len(self.menu_items) def draw(self, ctx): + animation_progress = ease_out_quart(self.animation_time_ms / self.speed_ms) + animation_direction = 1 if self.is_animating == "up" else -1 + # Current menu item - ctx.font_size = 40 + ctx.font_size = self.item_font_size + animation_progress * ( + self.focused_item_font_size - self.item_font_size + ) + ctx.text_align = ctx.CENTER ctx.text_baseline = ctx.MIDDLE - focused_item_margin = 20 - line_height = 30 ctx.rgb(1, 1, 1) - ctx.move_to(0, 0).text(self.menu_items[self.position % len(self.menu_items)]) + ctx.move_to( + 0, animation_direction * -30 + animation_progress * animation_direction * 30 + ).text(self.menu_items[self.position % len(self.menu_items)]) # Previous menu items ctx.font_size = 20 for i in range(1, 4): if (self.position - i) >= 0: - ctx.move_to(0, -focused_item_margin + -i * line_height).text( - self.menu_items[self.position - i] - ) + ctx.move_to( + 0, + -self.focused_item_margin + + -i * self.item_line_height + - animation_direction * 30 + + animation_progress * animation_direction * 30, + ).text(self.menu_items[self.position - i]) # Next menu items for i in range(1, 4): if (self.position + i) < len(self.menu_items): - ctx.move_to(0, focused_item_margin + i * line_height).text( - self.menu_items[self.position + i] - ) + ctx.move_to( + 0, + self.focused_item_margin + + i * self.item_line_height + - animation_direction * 30 + + animation_progress * animation_direction * 30, + ).text(self.menu_items[self.position + i]) + + def update(self, delta): + if self.is_animating != "none": + self.animation_time_ms += delta + if self.animation_time_ms > self.speed_ms: + self.is_animating = "none" + self.animation_time_ms = self.speed_ms diff --git a/modules/apps/menu_demo.py b/modules/apps/menu_demo.py index 76a3665..aea3f38 100644 --- a/modules/apps/menu_demo.py +++ b/modules/apps/menu_demo.py @@ -23,7 +23,6 @@ def __init__(self): ) def select_handler(self, item): - print(item) if item in ["numbers", "letters", "words", "main"]: self.set_menu(item) else: @@ -75,4 +74,4 @@ def draw(self, ctx): self.menu.draw(ctx) def update(self, delta): - pass + self.menu.update(delta)