From bf09cbf12ba8790596bff43267a35564abe93534 Mon Sep 17 00:00:00 2001 From: Thomas Farstrike Date: Thu, 30 Oct 2025 21:03:42 +0100 Subject: [PATCH] Launcher: only redraw if app list changed --- .../assets/launcher.py | 124 +++++++++++++----- 1 file changed, 94 insertions(+), 30 deletions(-) diff --git a/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py b/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py index aacac865..569b5af5 100644 --- a/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py +++ b/internal_filesystem/builtin/apps/com.micropythonos.launcher/assets/launcher.py @@ -8,91 +8,156 @@ # All icons took: 1457ms # All icons took: 1250ms # Most of this time is actually spent reading and parsing manifests. - -import uos import lvgl as lv - import mpos.apps import mpos.ui from mpos.content.pm import PackageManager from mpos import Activity +import time +import uhashlib +import ubinascii + class Launcher(Activity): + def __init__(self): + super().__init__() + # Cache of the last app list + a quick hash of the icons + self._last_app_list = None # list of tuples (name, path, icon_hash) + self._last_ui_built = False # was UI built at least once? def onCreate(self): print("launcher.py onCreate()") main_screen = lv.obj() main_screen.set_style_border_width(0, lv.PART.MAIN) main_screen.set_style_radius(0, 0) - main_screen.set_pos(0, mpos.ui.topmenu.NOTIFICATION_BAR_HEIGHT) # leave some margin for the notification bar - #main_screen.set_size(lv.pct(100), lv.pct(100)) + main_screen.set_pos(0, mpos.ui.topmenu.NOTIFICATION_BAR_HEIGHT) main_screen.set_style_pad_hor(mpos.ui.pct_of_display_width(2), 0) main_screen.set_style_pad_ver(mpos.ui.topmenu.NOTIFICATION_BAR_HEIGHT, 0) main_screen.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP) self.setContentView(main_screen) + # ------------------------------------------------------------------ + # Helper: compute a cheap hash of a file (or return None if missing) + @staticmethod + def _hash_file(path): + try: + with open(path, "rb") as f: + h = uhashlib.sha1() + while True: + data = f.read(1024) + if not data: + break + h.update(data) + return ubinascii.hexlify(h.digest()).decode() + except Exception: + return None + + # ------------------------------------------------------------------ def onResume(self, screen): - # Grid parameters - icon_size = 64 # Adjust based on your display - label_height = 24 - iconcont_width = icon_size + label_height - iconcont_height = icon_size + label_height + # ------------------------------------------------------------------ + # 1. Build a *compact* representation of the current app list + current_apps = [] + for app in PackageManager.get_app_list(): + if app.category == "launcher": + continue + icon_path = f"{app.installed_path}/res/mipmap-mdpi/icon_64x64.png" + icon_hash = Launcher._hash_file(icon_path) # cheap SHA-1 of the icon file + current_apps.append((app.name, app.installed_path, icon_hash)) - import time + # ------------------------------------------------------------------ + # 2. Compare with the cached list – if identical we skip UI rebuild start = time.ticks_ms() + rebuild_needed = True + if (self._last_app_list is not None and + len(self._last_app_list) == len(current_apps)): + # element-wise compare (name, path, icon_hash) + if all(a == b for a, b in zip(self._last_app_list, current_apps)): + rebuild_needed = False + + if not rebuild_needed: + end = time.ticks_ms() + print(f"Redraw icons took: {end-start}ms (cached – no change)") + return + + # ------------------------------------------------------------------ + # 3. UI needs (re)building – clear screen and create widgets screen.clean() - # Get the group for focusable objects focusgroup = lv.group_get_default() if not focusgroup: print("WARNING: could not get default focusgroup") - # Create UI for each app + # Grid parameters + icon_size = 64 + label_height = 24 + iconcont_width = icon_size + label_height + iconcont_height = icon_size + label_height + for app in PackageManager.get_app_list(): if app.category == "launcher": - print("Skipping launcher app from launcher apps...") continue + app_name = app.name app_dir_fullpath = app.installed_path print(f"Adding app {app_name} from {app_dir_fullpath}") - # Create container for each app (icon + label) + + # ----- container ------------------------------------------------ app_cont = lv.obj(screen) app_cont.set_size(iconcont_width, iconcont_height) app_cont.set_style_border_width(0, lv.PART.MAIN) app_cont.set_style_pad_all(0, 0) - app_cont.set_style_bg_opa(lv.OPA.TRANSP,0) # prevent default style from adding slight gray to this container + app_cont.set_style_bg_opa(lv.OPA.TRANSP, 0) app_cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) - # Load and display icon + + # ----- icon ---------------------------------------------------- icon_path = f"{app_dir_fullpath}/res/mipmap-mdpi/icon_64x64.png" image = lv.image(app_cont) try: image.set_src(Launcher.load_icon(icon_path)) except Exception as e: - print(f"Error loading icon {icon_path}: {e} - loading default icon") + print(f"Error loading icon {icon_path}: {e} - loading default") icon_path = "builtin/res/mipmap-mdpi/default_icon_64x64.png" try: image.set_src(Launcher.load_icon(icon_path)) except Exception as e: - print(f"Error loading default icon {icon_path}: {e} - using symbol") + print(f"Error loading default {icon_path}: {e} - using symbol") image.set_src(lv.SYMBOL.STOP) + image.align(lv.ALIGN.TOP_MID, 0, 0) image.set_size(icon_size, icon_size) + + # ----- label --------------------------------------------------- label = lv.label(app_cont) - label.set_text(app_name) # Use app_name directly + label.set_text(app_name) label.set_long_mode(lv.label.LONG_MODE.WRAP) label.set_width(iconcont_width) label.align(lv.ALIGN.BOTTOM_MID, 0, 0) label.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) - app_cont.add_event_cb(lambda e, fullname=app.fullname: mpos.apps.start_app(fullname), lv.EVENT.CLICKED, None) - app_cont.add_event_cb(lambda e, app_cont=app_cont: self.focus_app_cont(app_cont),lv.EVENT.FOCUSED,None) - app_cont.add_event_cb(lambda e, app_cont=app_cont: self.defocus_app_cont(app_cont),lv.EVENT.DEFOCUSED,None) + + # ----- events -------------------------------------------------- + app_cont.add_event_cb( + lambda e, fullname=app.fullname: mpos.apps.start_app(fullname), + lv.EVENT.CLICKED, None) + app_cont.add_event_cb( + lambda e, cont=app_cont: self.focus_app_cont(cont), + lv.EVENT.FOCUSED, None) + app_cont.add_event_cb( + lambda e, cont=app_cont: self.defocus_app_cont(cont), + lv.EVENT.DEFOCUSED, None) + if focusgroup: focusgroup.add_obj(app_cont) - - end = time.ticks_ms() - print(f"Redraw icons took: {end-start}ms") + # ------------------------------------------------------------------ + # 4. Store the new representation for the next resume + self._last_app_list = current_apps + self._last_ui_built = True + + end = time.ticks_ms() + print(f"Redraw icons took: {end-start}ms (full rebuild)") + + # ------------------------------------------------------------------ @staticmethod def load_icon(icon_path): with open(icon_path, 'rb') as f: @@ -103,12 +168,11 @@ class Launcher(Activity): }) return image_dsc + # ------------------------------------------------------------------ def focus_app_cont(self, app_cont): - #print(f"app_cont {app_cont} focused, setting border...") - app_cont.set_style_border_color(lv.theme_get_color_primary(None),lv.PART.MAIN) + app_cont.set_style_border_color(lv.theme_get_color_primary(None), lv.PART.MAIN) app_cont.set_style_border_width(1, lv.PART.MAIN) - app_cont.scroll_to_view(True) # scroll to bring it into view + app_cont.scroll_to_view(True) def defocus_app_cont(self, app_cont): - #print(f"app_cont {app_cont} defocused, unsetting border...") app_cont.set_style_border_width(0, lv.PART.MAIN)