diff --git a/lib/luavgl/src/font.c b/lib/luavgl/src/font.c index 87e0bbef..7751e0c7 100644 --- a/lib/luavgl/src/font.c +++ b/lib/luavgl/src/font.c @@ -1,49 +1,23 @@ #include "luavgl.h" #include "private.h" -static char *to_lower(char *str) -{ - for (char *s = str; *s; ++s) - *s = *s >= 'A' && *s <= 'Z' ? *s | 0x60 : *s; - return str; -} - -static char *luavgl_strchr(const char *s, char c) -{ - while (*s) { - if (c == *s) { - return (char *)s; - } - s++; - } - return NULL; -} - -/** - * Dynamic font family fallback is not supported. - * The fallback only happen when font creation fails and continue to try next - * one. Fallback logic in lvgl is supposed to be system wide. - * - * lvgl.Font("MiSansW medium, montserrat", 24, "normal") - */ static int luavgl_font_create(lua_State *L) { - if (!lua_isstring(L, 1)) { return luaL_argerror(L, 1, "expect string"); } - const char *name = lua_tostring(L, 1); - const lv_font_t *font = NULL; + if (!lua_isfunction(L, 2)) { + return luaL_argerror(L, 1, "expect function"); + } luavgl_ctx_t *ctx = luavgl_context(L); - if (ctx->make_font) { - font = ctx->make_font(name); + if (!ctx->make_font) { + return luaL_error(L, "cannot create font"); } - if (font) { - lua_pushlightuserdata(L, (void *)font); - return 1; - } + const char *name = lua_tostring(L, 1); + int cb_ref = luaL_ref(L, LUA_REGISTRYINDEX); + ctx->make_font(L, name, cb_ref); - return luaL_error(L, "cannot create font"); + return 0; } diff --git a/lib/luavgl/src/fs.c b/lib/luavgl/src/fs.c index 4deec5e5..4918dcb6 100644 --- a/lib/luavgl/src/fs.c +++ b/lib/luavgl/src/fs.c @@ -1,6 +1,17 @@ #include "luavgl.h" #include "private.h" +static char *luavgl_strchr(const char *s, char c) +{ + while (*s) { + if (c == *s) { + return (char *)s; + } + s++; + } + return NULL; +} + typedef struct luavgl_fs_s { lv_fs_file_t file; bool closed; /* userdata exists but lv_fs has been closed */ diff --git a/lib/luavgl/src/luavgl.h b/lib/luavgl/src/luavgl.h index 6c5f8e98..8b7b92aa 100644 --- a/lib/luavgl/src/luavgl.h +++ b/lib/luavgl/src/luavgl.h @@ -12,7 +12,7 @@ extern "C" { #endif -typedef const lv_font_t *(*make_font_cb)(const char *); +typedef void (*make_font_cb)(lua_State *L, const char *, int cb); typedef void (*delete_font_cb)(const lv_font_t *); typedef int (*luavgl_pcall_t)(lua_State *L, int nargs, int nresults); diff --git a/lua/font.lua b/lua/font.lua index 7afa1e01..c76602ec 100644 --- a/lua/font.lua +++ b/lua/font.lua @@ -1,6 +1,36 @@ local lvgl = require("lvgl") -return { - fusion_12 = lvgl.Font("//lua/fonts/fusion12"), - fusion_10 = lvgl.Font("//lua/fonts/fusion10"), +local fonts = {} +local fonts_priv = { + has_invoked_cb = false, + cb = nil, } + +function fonts_priv.has_loaded_all() + return fonts.fusion_12 and fonts.fusion_10 +end + +function fonts_priv.invoke_cb() + if fonts_priv.has_invoked_cb or not fonts_priv.cb then return end + if not fonts_priv.has_loaded_all() then return end + fonts_priv.has_invoked_cb = true + fonts_priv.cb() +end + +lvgl.Font("//lua/fonts/fusion12", function(font) + fonts.fusion_12 = font + fonts_priv.invoke_cb() +end) + +lvgl.Font("//lua/fonts/fusion10", function(font) + fonts.fusion_10 = font + fonts_priv.invoke_cb() +end) + +function fonts.on_loaded(cb) + fonts_priv.cb = cb + fonts_priv.has_invoked_cb = false + fonts_priv.invoke_cb() +end + +return fonts diff --git a/lua/main.lua b/lua/main.lua index 1540004d..4cd754b6 100644 --- a/lua/main.lua +++ b/lua/main.lua @@ -1,4 +1,10 @@ +-- Load fonts first, since they're parsed asynchronously and we can do much of +-- the other UI setup in parallel. local font = require("font") + +-- require() everything else needed for the main menu + global bindings. Do +-- this now instead of in init_ui because loading and parsing the scripts can +-- take a while. local vol = require("volume") local theme = require("theme") local controls = require("controls") @@ -7,55 +13,66 @@ local sd_card = require("sd_card") local backstack = require("backstack") local main_menu = require("main_menu") -local theme_dark = require("theme_dark") -theme.set(theme_dark) +local function init_ui() + -- Load the theme within init_ui because the theme needs fonts to be ready. + local theme_dark = require("theme_dark") + theme.set(theme_dark) -local lock_time = time.ticks() + local lock_time = time.ticks() --- Set up property bindings that are used across every screen. -GLOBAL_BINDINGS = { - -- Show an alert with the current volume whenever the volume changes - vol.current_pct:bind(function(pct) - require("alerts").show(function() - local container = lvgl.Object(nil, { - w = lvgl.PCT(80), - h = lvgl.SIZE_CONTENT, - flex = { - flex_direction = "column", - justify_content = "center", - align_items = "center", - align_content = "center", - }, - radius = 8, - pad_all = 2, - }) - theme.set_style(container, "pop_up") - container:Label { - text = string.format("Volume %i%%", pct), - text_font = font.fusion_10 - } - container:Bar { - w = lvgl.PCT(100), - h = 8, - range = { min = 0, max = 100 }, - value = pct, - } - container:center() - end) - end), - -- When the device has been locked for a while, default to showing the now - -- playing screen after unlocking. - controls.lock_switch:bind(function(locked) - if locked then - lock_time = time.ticks() - elseif time.ticks() - lock_time > 8000 then - local queue = require("queue") - if queue.size:get() > 0 then - require("playing"):pushIfNotShown() + -- Set up property bindings that are used across every screen. + GLOBAL_BINDINGS = { + -- Show an alert with the current volume whenever the volume changes + vol.current_pct:bind(function(pct) + require("alerts").show(function() + local container = lvgl.Object(nil, { + w = lvgl.PCT(80), + h = lvgl.SIZE_CONTENT, + flex = { + flex_direction = "column", + justify_content = "center", + align_items = "center", + align_content = "center", + }, + radius = 8, + pad_all = 2, + }) + theme.set_style(container, "pop_up") + container:Label { + text = string.format("Volume %i%%", pct), + text_font = font.fusion_10 + } + container:Bar { + w = lvgl.PCT(100), + h = 8, + range = { min = 0, max = 100 }, + value = pct, + } + container:center() + end) + end), + -- When the device has been locked for a while, default to showing the now + -- playing screen after unlocking. + controls.lock_switch:bind(function(locked) + if locked then + lock_time = time.ticks() + elseif time.ticks() - lock_time > 8000 then + local queue = require("queue") + if queue.size:get() > 0 then + require("playing"):pushIfNotShown() + end end - end - end), - sd_card.mounted:bind(function(mounted) - backstack.reset(main_menu:new()) - end), -} + end), + sd_card.mounted:bind(function(mounted) + backstack.reset(main_menu:new()) + end), + } +end + +-- Wait for fonts to finish, then show the main menu. +-- We could show an intermediary Lua-controlled splash/loading UI whilst we +-- wait, but in practice loading the fonts takes only a few hundred ms longer +-- than all the other UI init. +font.on_loaded(function() + init_ui() +end) diff --git a/src/tangara/lua/bridge.cpp b/src/tangara/lua/bridge.cpp index 0f1e65ac..1c757a22 100644 --- a/src/tangara/lua/bridge.cpp +++ b/src/tangara/lua/bridge.cpp @@ -24,6 +24,7 @@ #include "lua/lua_controls.hpp" #include "lua/lua_database.hpp" #include "lua/lua_filesystem.hpp" +#include "lua/lua_font.hpp" #include "lua/lua_queue.hpp" #include "lua/lua_screen.hpp" #include "lua/lua_testing.hpp" @@ -50,65 +51,6 @@ namespace lua { static constexpr char kBridgeKey[] = "bridge"; -static auto make_font_cb(const char* name) -> const lv_font_t* { - // Most Lua file paths start with "//" in order to deal with LVGL's Windows-y - // approach to paths. Try to handle such paths correctly so that paths in Lua - // code look a bit more consistent. - { - std::string name_str = name; - if (name_str.starts_with("//")) { - name++; - } - } - - // This following is a bit C-brained. Sorry. - - ESP_LOGI(kTag, "load font '%s'", name); - FILE* f = fopen(name, "r"); - if (!f) { - return NULL; - } - - uint8_t* data = NULL; - long len = 0; - lv_font_t* font = NULL; - - if (fseek(f, 0, SEEK_END)) { - goto fail_with_file; - } - - len = ftell(f); - if (len <= 0) { - goto fail_with_file; - } - - if (fseek(f, 0, SEEK_SET)) { - goto fail_with_file; - } - - data = reinterpret_cast(heap_caps_malloc(len, MALLOC_CAP_SPIRAM)); - if (!data) { - goto fail_with_buffer; - } - - if (fread(data, 1, len, f) < len) { - goto fail_with_buffer; - } - - // We can finally start parsing the font! - font = lv_binfont_create_from_buffer(data, len); - -fail_with_buffer: - // LVGL copies the font data out of the buffer, so we don't need to big raw - // data alloc after this function returns. - heap_caps_free(data); - -fail_with_file: - fclose(f); - - return font; -} - static auto delete_font_cb(const lv_font_t* font) -> void { // FIXME: luavgl never actually calls this? } @@ -146,7 +88,7 @@ auto Bridge::installBaseModules(lua_State* L) -> void { auto Bridge::installLvgl(lua_State* L) -> void { luavgl_set_pcall(L, CallProtected); - luavgl_set_font_extension(L, make_font_cb, delete_font_cb); + luavgl_set_font_extension(L, loadFont, delete_font_cb); luaL_requiref(L, "lvgl", luaopen_lvgl, true); lua_pop(L, 1); } diff --git a/src/tangara/lua/lua_font.cpp b/src/tangara/lua/lua_font.cpp new file mode 100644 index 00000000..e5913492 --- /dev/null +++ b/src/tangara/lua/lua_font.cpp @@ -0,0 +1,115 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "lua_font.hpp" + +#include +#include + +#include "lauxlib.h" +#include "lua.h" +#include "lvgl.h" + +#include "events/event_queue.hpp" +#include "lua/bridge.hpp" +#include "lua/lua_registry.hpp" +#include "lua/lua_thread.hpp" + +namespace lua { + +[[maybe_unused]] static constexpr char kTag[] = "lua_font"; + +/* Reads the given file completely into PSRAM. */ +static auto readFont(std::string path) -> std::span { + // This following is a bit C-brained. Sorry. + FILE* f = fopen(path.c_str(), "r"); + if (!f) { + return {}; + } + + uint8_t* data = NULL; + long len = 0; + + if (fseek(f, 0, SEEK_END)) { + goto fail; + } + + len = ftell(f); + if (len <= 0) { + goto fail; + } + + if (fseek(f, 0, SEEK_SET)) { + len = 0; + goto fail; + } + + data = reinterpret_cast(heap_caps_malloc(len, MALLOC_CAP_SPIRAM)); + if (!data) { + len = 0; + goto fail; + } + + if (fread(data, 1, len, f) < len) { + heap_caps_free(data); + len = 0; + } + +fail: + fclose(f); + + return {data, static_cast(len)}; +} + +static auto parseFont(std::span data) -> lv_font_t* { + if (data.empty()) { + return nullptr; + } + + lv_font_t* font = lv_binfont_create_from_buffer(data.data(), data.size()); + heap_caps_free(data.data()); + + return font; +} + +auto loadFont(lua_State* L, const char* path, int cb_ref) -> void { + // Most Lua file paths start with "//" in order to deal with LVGL's Windows-y + // approach to paths. Try to handle such paths correctly so that paths in Lua + // code look a bit more consistent. + std::string path_str = path; + if (path_str.starts_with("//")) { + path++; + path_str = path; + } + + // Do the file read from the current thread, since the path might be for a + // file in flash, and we can't read from flash in a background task. + auto font_data = readFont(path_str); + + Bridge* bridge = Bridge::Get(L); + bridge->services().bg_worker().Dispatch([=]() { + // Do the parsing now that we're in the background. + lv_font_t* font = parseFont(font_data); + + // Hop back to the UI task to invoke the Lua callback. + events::Ui().RunOnTask([=] { + // Retrieve the callback by ref, and release the ref. + lua_rawgeti(L, LUA_REGISTRYINDEX, cb_ref); + luaL_unref(L, LUA_REGISTRYINDEX, cb_ref); + + // We always invoke the callback, but we don't always have a result. + if (font) { + lua_pushlightuserdata(L, (void*)font); + } else { + lua_pushnil(L); + } + + CallProtected(L, 1, 0); + }); + }); +} + +} // namespace lua diff --git a/src/tangara/lua/lua_font.hpp b/src/tangara/lua/lua_font.hpp new file mode 100644 index 00000000..dfec4eb0 --- /dev/null +++ b/src/tangara/lua/lua_font.hpp @@ -0,0 +1,15 @@ +/* + * Copyright 2024 jacqueline + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#pragma once + +#include "lua.hpp" + +namespace lua { + +auto loadFont(lua_State* L, const char* name, int cb_ref) -> void; + +}