From 60acc168cf0a653be5c4692f41129ef99f816f20 Mon Sep 17 00:00:00 2001 From: Xi Chen Date: Fri, 21 Aug 2026 08:54:40 +0200 Subject: [PATCH] Added LVGL Montserrat typography --- AGENTS.md | 4 +- README.md | 3 +- components/buddy_ui/CMakeLists.txt | 3 +- components/buddy_ui/buddy_ui.cpp | 68 +++++++---- components/codex_ui/CMakeLists.txt | 3 +- components/codex_ui/codex_ui.cpp | 89 +++++++++------ .../platform_core_s3/platform_core_s3.cpp | 8 +- components/ui_font/CMakeLists.txt | 5 + components/ui_font/include/ui_font.hpp | 16 +++ components/ui_font/ui_font.cpp | 108 ++++++++++++++++++ dependencies.lock | 10 +- docs/images/buddy-selector.png | Bin 1653 -> 10359 bytes docs/images/claude-activity.png | Bin 2067 -> 10368 bytes docs/images/claude-clock.png | Bin 1322 -> 6742 bytes docs/images/claude-info.png | Bin 1616 -> 8799 bytes docs/images/claude-owl.png | Bin 1849 -> 7123 bytes docs/images/codex-agents.png | Bin 2499 -> 13314 bytes docs/images/codex-control.png | Bin 1771 -> 10290 bytes docs/images/codex-navigate.png | Bin 1690 -> 9039 bytes main/idf_component.yml | 1 + scripts/render_ui_previews.sh | 18 +++ sdkconfig.defaults | 8 ++ test/host/test_codex_ui.cpp | 22 ++-- test/host/ui_preview/CMakeLists.txt | 38 ++++++ test/host/ui_preview/lv_conf.h | 19 +++ 25 files changed, 345 insertions(+), 78 deletions(-) create mode 100644 components/ui_font/CMakeLists.txt create mode 100644 components/ui_font/include/ui_font.hpp create mode 100644 components/ui_font/ui_font.cpp create mode 100644 scripts/render_ui_previews.sh create mode 100644 test/host/ui_preview/CMakeLists.txt create mode 100644 test/host/ui_preview/lv_conf.h diff --git a/AGENTS.md b/AGENTS.md index 3220c75..c6cdcce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,9 @@ - Explain meaningful design choices in plain language so maintainers can review and modify the code. - Prefer small, readable changes over broad rewrites or generated abstractions. -- Ask before changing hardware pin assignments, partition layouts, storage formats, or dependencies. +- Ask before changing hardware pin assignments, partition layouts, storage formats, + or non-UI dependencies. Focused UI libraries are allowed when they materially + improve the on-device experience; document why they are needed. - Never commit secrets, Wi-Fi credentials, private keys, or device-specific provisioning data. ## Code conventions diff --git a/README.md b/README.md index f8498b3..08c6a23 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,8 @@ The device asks which buddy to start before enabling Bluetooth: | Claude Owl page | Claude Activity page | Claude Clock page | Claude Info page | These previews come from the same 320 x 240 RGB565 renderers used by the -physical display. +physical display, including LVGL's anti-aliased Montserrat fonts. Regenerate +them with `sh scripts/render_ui_previews.sh` after UI changes. ## Using Codex mode diff --git a/components/buddy_ui/CMakeLists.txt b/components/buddy_ui/CMakeLists.txt index e6c1e46..bddb57d 100644 --- a/components/buddy_ui/CMakeLists.txt +++ b/components/buddy_ui/CMakeLists.txt @@ -1,5 +1,6 @@ idf_component_register(SRCS "buddy_ui.cpp" INCLUDE_DIRS "include" - REQUIRES claude_model) + REQUIRES claude_model ui_font) target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror) +target_compile_definitions(${COMPONENT_LIB} PRIVATE BUDDY_USE_LVGL_FONT=1) diff --git a/components/buddy_ui/buddy_ui.cpp b/components/buddy_ui/buddy_ui.cpp index 6a3350c..7199e28 100644 --- a/components/buddy_ui/buddy_ui.cpp +++ b/components/buddy_ui/buddy_ui.cpp @@ -5,6 +5,10 @@ #include #include +#ifdef BUDDY_USE_LVGL_FONT +#include "ui_font.hpp" +#endif + namespace buddy::display { constexpr std::uint16_t rgb565(unsigned red, unsigned green, @@ -14,19 +18,20 @@ constexpr std::uint16_t rgb565(unsigned red, unsigned green, ((red & 0xf8U) << 8) | ((green & 0xfcU) << 3) | (blue >> 3)); } -constexpr auto Background = rgb565(9, 13, 22); -constexpr auto Panel = rgb565(22, 30, 45); -constexpr auto Text = rgb565(242, 246, 252); -constexpr auto Muted = rgb565(146, 159, 180); -constexpr auto Dim = rgb565(76, 89, 108); -constexpr auto CodexColor = rgb565(45, 145, 235); -constexpr auto ClaudeColor = rgb565(215, 118, 76); -constexpr auto Owl = rgb565(184, 125, 73); -constexpr auto OwlLight = rgb565(233, 195, 133); -constexpr auto Amber = rgb565(245, 170, 40); -constexpr auto Green = rgb565(35, 190, 100); -constexpr auto Red = rgb565(230, 65, 75); -constexpr auto Cyan = rgb565(45, 190, 225); +constexpr auto Background = rgb565(16, 19, 24); +constexpr auto Panel = rgb565(25, 30, 37); +constexpr auto Outline = rgb565(53, 61, 71); +constexpr auto Text = rgb565(199, 206, 214); +constexpr auto Muted = rgb565(145, 155, 167); +constexpr auto Dim = rgb565(82, 92, 104); +constexpr auto CodexColor = rgb565(75, 135, 188); +constexpr auto ClaudeColor = rgb565(181, 105, 72); +constexpr auto Owl = rgb565(161, 111, 69); +constexpr auto OwlLight = rgb565(206, 174, 119); +constexpr auto Amber = rgb565(181, 132, 68); +constexpr auto Green = rgb565(67, 146, 101); +constexpr auto Red = rgb565(181, 83, 89); +constexpr auto Cyan = rgb565(73, 153, 174); constexpr int TabTop = 210; struct Canvas { @@ -104,6 +109,16 @@ static void box(Canvas *canvas, int x, int y, int width, int height, rounded(canvas, x + 2, y + 2, width - 4, height - 4, 6, fill); } +/* Draws a quiet surface card with a short semantic accent along its top. */ +static void card(Canvas *canvas, int x, int y, int width, int height, + uint16_t accent) +{ + rounded(canvas, x, y, width, height, 8, Outline); + rounded(canvas, x + 1, y + 1, width - 2, height - 2, 7, Panel); + rounded(canvas, x + 18, y + 1, width - 36, 4, 2, accent); +} + +#ifndef BUDDY_USE_LVGL_FONT /* Returns the five-column bitmap for a supported character or fallback glyph. */ static const uint8_t *glyph(char value) { @@ -148,12 +163,17 @@ static const uint8_t *glyph(char value) if (value == '!') return exclamation; return blank; } +#endif /* Calculates pixel width for fixed 5x7 glyphs plus spacing. */ static int text_width(const char *text, int scale) { +#ifdef BUDDY_USE_LVGL_FONT + return text == nullptr ? 0 : buddy::font::textWidth(text, scale); +#else return text == nullptr || text[0] == '\0' ? 0 : static_cast(strlen(text)) * 6 * scale - scale; +#endif } /* Rasterizes scaled bitmap text at the requested top-left coordinate. */ @@ -161,6 +181,10 @@ static void text(Canvas *canvas, const char *value, int x, int y, int scale, uint16_t color) { if (value == nullptr) return; +#ifdef BUDDY_USE_LVGL_FONT + buddy::font::draw({canvas->pixels, PixelCount}, Width, Height, value, + x, y, scale, color); +#else for (; *value != '\0'; ++value, x += 6 * scale) { const uint8_t *columns = glyph(*value); for (int column = 0; column < 5; ++column) @@ -169,6 +193,7 @@ static void text(Canvas *canvas, const char *value, int x, int y, int scale, rect(canvas, x + column * scale, y + row * scale, scale, scale, color); } +#endif } /* Centers bitmap text horizontally around a requested coordinate. */ @@ -212,13 +237,13 @@ void renderSelector(std::span pixels) noexcept centered(&canvas, "CHOOSE YOUR BUDDY", 160, 18, 2, Text); centered(&canvas, "BLUETOOTH STARTS AFTER SELECTION", 160, 43, 1, Muted); - box(&canvas, 12, 66, 142, 135, CodexColor, Panel); + card(&canvas, 12, 66, 142, 135, CodexColor); centered(&canvas, "CODEX", 83, 84, 3, Text); centered(&canvas, "6 AGENTS", 83, 124, 2, CodexColor); centered(&canvas, "CHATGPT", 83, 158, 1, Muted); centered(&canvas, "TAP TO START", 83, 179, 1, Text); - box(&canvas, 166, 66, 142, 135, ClaudeColor, Panel); + card(&canvas, 166, 66, 142, 135, ClaudeColor); draw_mini_owl(&canvas, 237, 99, Amber); centered(&canvas, "CLAUDE", 237, 124, 2, Text); centered(&canvas, "OWL BUDDY", 237, 151, 1, ClaudeColor); @@ -329,17 +354,16 @@ static void draw_owl(Canvas *canvas, buddy::claude::PetState state, } } -/* Draws the Claude title and live/secure connection badges. */ +/* Draws the Claude title and live connection status. */ static void draw_header(Canvas *canvas, const buddy::claude::Model *model, bool battery_known, uint8_t battery_percent, bool charging) { rect(canvas, 0, 0, Width, 30, Panel); text(canvas, "CLAUDE", 7, 8, 2, Text); - draw_mini_owl(canvas, 88, 15, ClaudeColor); const bool connected = model->connection == buddy::claude::Connection::Connected; - circle(canvas, 239, 15, 4, connected ? Green : Dim); - text(canvas, connected ? "LIVE" : "PAIR", 249, 12, 1, + circle(canvas, 247, 15, 4, connected ? Green : Dim); + text(canvas, connected ? "LIVE" : "PAIR", 257, 12, 1, connected ? Text : Muted); if (battery_known) { char battery[8]; @@ -443,9 +467,8 @@ static void draw_clock_page(Canvas *canvas, const buddy::claude::Model *model, { int64_t local = 0; if (!buddy::claude::clockSeconds(*model, now_ms, local) || local < 0) { - draw_mini_owl(canvas, 160, 87, ClaudeColor); - centered(canvas, "WAITING FOR TIME", 160, 121, 2, Text); - centered(canvas, "CLAUDE WILL SYNC THE CLOCK", 160, 151, 1, Muted); + centered(canvas, "WAITING FOR TIME", 160, 87, 2, Text); + centered(canvas, "CLAUDE WILL SYNC THE CLOCK", 160, 121, 1, Muted); return; } const int64_t days = local / 86400; @@ -471,7 +494,6 @@ static void draw_clock_page(Canvas *canvas, const buddy::claude::Model *model, snprintf(date, sizeof(date), "%s %04d-%02u-%02u", weekdays[weekday], year, month, day); centered(canvas, date, 160, 132, 2, OwlLight); - draw_mini_owl(canvas, 160, 178, ClaudeColor); } /* Draws device identity, decision statistics, and the mode-switch control. */ diff --git a/components/codex_ui/CMakeLists.txt b/components/codex_ui/CMakeLists.txt index 32920be..dcd34be 100644 --- a/components/codex_ui/CMakeLists.txt +++ b/components/codex_ui/CMakeLists.txt @@ -1,5 +1,6 @@ idf_component_register(SRCS "codex_ui.cpp" INCLUDE_DIRS "include" - REQUIRES codex_input codex_model) + REQUIRES codex_input codex_model ui_font) target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror) +target_compile_definitions(${COMPONENT_LIB} PRIVATE BUDDY_USE_LVGL_FONT=1) diff --git a/components/codex_ui/codex_ui.cpp b/components/codex_ui/codex_ui.cpp index b8e5d24..611bfb0 100644 --- a/components/codex_ui/codex_ui.cpp +++ b/components/codex_ui/codex_ui.cpp @@ -3,6 +3,10 @@ #include #include +#ifdef BUDDY_USE_LVGL_FONT +#include "ui_font.hpp" +#endif + namespace buddy::codex::ui { constexpr std::uint16_t rgb565(unsigned red, unsigned green, @@ -12,23 +16,24 @@ constexpr std::uint16_t rgb565(unsigned red, unsigned green, ((red & 0xf8U) << 8) | ((green & 0xfcU) << 3) | (blue >> 3)); } -constexpr auto Background = rgb565(7, 11, 18); -constexpr auto Panel = rgb565(19, 27, 40); -constexpr auto PanelPressed = rgb565(42, 56, 76); -constexpr auto PanelDisabled = rgb565(14, 20, 29); -constexpr auto Text = rgb565(242, 246, 252); -constexpr auto Muted = rgb565(150, 162, 181); -constexpr auto Dim = rgb565(83, 94, 111); -constexpr auto Accent = rgb565(45, 145, 235); -constexpr auto Cyan = rgb565(45, 190, 225); -constexpr auto Green = rgb565(35, 190, 100); -constexpr auto Red = rgb565(230, 65, 75); -constexpr auto Amber = rgb565(245, 170, 40); -constexpr auto TintBlue = rgb565(15, 42, 65); -constexpr auto TintCyan = rgb565(13, 48, 62); -constexpr auto TintGreen = rgb565(12, 47, 31); -constexpr auto TintRed = rgb565(54, 22, 29); -constexpr auto TintAmber = rgb565(55, 39, 17); +constexpr auto Background = rgb565(16, 19, 24); +constexpr auto Panel = rgb565(25, 30, 37); +constexpr auto PanelPressed = rgb565(41, 49, 59); +constexpr auto PanelDisabled = rgb565(20, 24, 30); +constexpr auto Outline = rgb565(53, 61, 71); +constexpr auto Text = rgb565(199, 206, 214); +constexpr auto Muted = rgb565(145, 155, 167); +constexpr auto Dim = rgb565(82, 92, 104); +constexpr auto Accent = rgb565(75, 135, 188); +constexpr auto Cyan = rgb565(73, 153, 174); +constexpr auto Green = rgb565(67, 146, 101); +constexpr auto Red = rgb565(181, 83, 89); +constexpr auto Amber = rgb565(181, 132, 68); +constexpr auto TintBlue = rgb565(23, 36, 48); +constexpr auto TintCyan = rgb565(22, 39, 46); +constexpr auto TintGreen = rgb565(22, 38, 30); +constexpr auto TintRed = rgb565(43, 27, 31); +constexpr auto TintAmber = rgb565(43, 35, 23); constexpr int TabTop = 210; struct Canvas { @@ -87,6 +92,7 @@ static void rounded_box(Canvas *canvas, int x, int y, int width, int height, radius - thickness, background); } +#ifndef BUDDY_USE_LVGL_FONT /* Returns the five-column bitmap for a supported character or fallback glyph. */ static const uint8_t *glyph(char value) { @@ -121,12 +127,17 @@ static const uint8_t *glyph(char value) if (value == '/') return slash; return blank; } +#endif /* Calculates pixel width for fixed 5x7 glyphs plus one-column spacing. */ static int text_width(const char *text, int scale) { +#ifdef BUDDY_USE_LVGL_FONT + return text == nullptr ? 0 : buddy::font::textWidth(text, scale); +#else return text == nullptr ? 0 : static_cast(strlen(text)) * 6 * scale - scale; +#endif } /* Rasterizes scaled bitmap text from left to right at a baseline origin. */ @@ -134,6 +145,10 @@ static void draw_text(Canvas *canvas, const char *text, int x, int y, int scale, uint16_t color) { if (text == nullptr || scale <= 0) return; +#ifdef BUDDY_USE_LVGL_FONT + buddy::font::draw({canvas->pixels, PixelCount}, Width, Height, text, + x, y, scale, color); +#else for (; *text != '\0'; ++text, x += 6 * scale) { const uint8_t *columns = glyph(*text); for (int column = 0; column < 5; ++column) @@ -142,6 +157,7 @@ static void draw_text(Canvas *canvas, const char *text, int x, int y, fill_rect(canvas, x + column * scale, y + row * scale, scale, scale, color); } +#endif } /* Centers bitmap text horizontally around a requested x coordinate. */ @@ -184,10 +200,12 @@ static void button(Canvas *canvas, int x, int y, int width, int height, const char *label, const char *hint, uint16_t border, uint16_t background, bool pressed, bool enabled) { - rounded_box(canvas, x, y, width, height, 7, 2, - enabled ? border : Dim, + rounded_box(canvas, x, y, width, height, 7, 1, + enabled ? (pressed ? border : Outline) : Dim, enabled ? (pressed ? PanelPressed : background) : PanelDisabled); + if (enabled) + fill_rounded_rect(canvas, x + 12, y + 1, width - 24, 3, 1, border); const int scale = strlen(label) > 8 ? 1 : 2; centered_text(canvas, label, x + width / 2, y + (hint == nullptr ? height / 2 - 7 * scale / 2 : height / 2 - 13), @@ -230,20 +248,20 @@ static uint16_t slot_accent(const Slot *slot, uint32_t time_ms) { /* Stable semantic colors keep status readable across host lighting themes. Unknown future states retain the raw host color as a fallback. */ - unsigned red = 83; - unsigned green = 94; - unsigned blue = 111; + unsigned red = 82; + unsigned green = 92; + unsigned blue = 104; switch (slot->status) { - case SlotStatus::Idle: red = 45; green = 145; blue = 235; break; - case SlotStatus::Thinking: red = 45; green = 190; blue = 225; break; - case SlotStatus::Complete: red = 35; green = 190; blue = 100; break; - case SlotStatus::RequiresInput: red = 245; green = 170; blue = 40; break; - case SlotStatus::Error: red = 230; green = 65; blue = 75; break; + case SlotStatus::Idle: red = 75; green = 135; blue = 188; break; + case SlotStatus::Thinking: red = 73; green = 153; blue = 174; break; + case SlotStatus::Complete: red = 67; green = 146; blue = 101; break; + case SlotStatus::RequiresInput: red = 181; green = 132; blue = 68; break; + case SlotStatus::Error: red = 181; green = 83; blue = 89; break; case SlotStatus::Unknown: red = (slot->color >> 16) & 0xffU; green = (slot->color >> 8) & 0xffU; blue = slot->color & 0xffU; - if (red + green + blue < 80U) red = green = blue = 111U; + if (red + green + blue < 80U) red = green = blue = 104U; break; case SlotStatus::Unassigned: break; } @@ -292,7 +310,7 @@ static void draw_header(Canvas *canvas, const Model *model) const uint16_t connection_color = connected ? Green : model->connection == Connection::Connecting ? Amber : Dim; - rounded_box(canvas, 245, 6, 68, 18, 8, 1, Dim, Background); + rounded_box(canvas, 245, 6, 68, 18, 8, 1, Outline, Background); fill_circle(canvas, 256, 15, 4, connection_color); draw_text(canvas, connected ? "LIVE" : "PAIR", 266, 12, 1, connected ? Text : Muted); @@ -368,21 +386,20 @@ static void draw_agents(Canvas *canvas, const Model *model, const Slot *slot = &model->slots[index]; const uint16_t accent = slot_accent(slot, time_ms); const bool enabled = model->connection == Connection::Connected; - rounded_box(canvas, x, y, 100, 76, 7, 2, - enabled ? accent : Dim, + rounded_box(canvas, x, y, 100, 76, 7, 1, + enabled ? (model->selectedSlot == index ? Text : Outline) + : Dim, action_matches(active, ActionType::Slot, index) ? PanelPressed : slot_background(slot->status)); - fill_rect(canvas, x + 2, y + 9, 4, 58, accent); + fill_rounded_rect(canvas, x + 12, y + 1, 76, 3, 1, accent); draw_text(canvas, number, x + 12, y + 9, 1, accent); fill_circle(canvas, x + 88, y + 13, 4, accent); centered_text(canvas, label, x + 50, y + 28, 2, enabled ? Text : Muted); centered_text(canvas, slotStatusLabel(slot->status), x + 50, y + 58, 1, enabled ? accent : Dim); - if (model->selectedSlot == index) { - fill_rect(canvas, x + 78, y, 22, 3, Text); - fill_rect(canvas, x + 97, y, 3, 20, Text); - } + if (model->selectedSlot == index) + fill_circle(canvas, x + 88, y + 13, 2, Text); } } diff --git a/components/platform_core_s3/platform_core_s3.cpp b/components/platform_core_s3/platform_core_s3.cpp index f9d3fc5..00f5d16 100644 --- a/components/platform_core_s3/platform_core_s3.cpp +++ b/components/platform_core_s3/platform_core_s3.cpp @@ -18,6 +18,7 @@ namespace buddy::platform { static const char *TAG = "platform_core_s3"; +static constexpr int ComfortableBrightness = 25; static esp_lcd_panel_handle_t s_panel; static esp_lcd_panel_io_handle_t s_panel_io; static esp_lcd_touch_handle_t s_touch; @@ -78,7 +79,7 @@ esp_err_t initialize() noexcept s_panel_io, &callbacks, nullptr)) != ESP_OK || (error = bsp_display_brightness_init()) != ESP_OK || (error = esp_lcd_panel_disp_on_off(s_panel, true)) != ESP_OK || - (error = bsp_display_backlight_on()) != ESP_OK || + (error = bsp_display_brightness_set(ComfortableBrightness)) != ESP_OK || (error = bsp_touch_new(nullptr, &s_touch)) != ESP_OK) { ESP_LOGE(TAG, "Display/touch initialization failed: %s", esp_err_to_name(error)); @@ -126,9 +127,10 @@ esp_err_t setDisplayAwake(bool awake) noexcept esp_err_t error; if (awake) { error = esp_lcd_panel_disp_on_off(s_panel, true); - if (error == ESP_OK) error = bsp_display_backlight_on(); + if (error == ESP_OK) + error = bsp_display_brightness_set(ComfortableBrightness); } else { - error = bsp_display_backlight_off(); + error = bsp_display_brightness_set(0); if (error == ESP_OK) error = esp_lcd_panel_disp_on_off(s_panel, false); } return error; diff --git a/components/ui_font/CMakeLists.txt b/components/ui_font/CMakeLists.txt new file mode 100644 index 0000000..0f15270 --- /dev/null +++ b/components/ui_font/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register(SRCS "ui_font.cpp" + INCLUDE_DIRS "include" + REQUIRES lvgl) + +target_compile_options(${COMPONENT_LIB} PRIVATE -Wall -Wextra -Werror) diff --git a/components/ui_font/include/ui_font.hpp b/components/ui_font/include/ui_font.hpp new file mode 100644 index 0000000..7d8a873 --- /dev/null +++ b/components/ui_font/include/ui_font.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +namespace buddy::font { + +/* Measures and rasterizes LVGL Montserrat text into an RGB565 framebuffer. */ +[[nodiscard]] int textWidth(std::string_view value, int scale) noexcept; + +void draw(std::span pixels, int canvasWidth, int canvasHeight, + std::string_view value, int x, int y, int scale, + std::uint16_t color) noexcept; + +} // namespace buddy::font diff --git a/components/ui_font/ui_font.cpp b/components/ui_font/ui_font.cpp new file mode 100644 index 0000000..a8b1a5d --- /dev/null +++ b/components/ui_font/ui_font.cpp @@ -0,0 +1,108 @@ +#include "ui_font.hpp" + +#include + +#include "lvgl.h" + +namespace buddy::font { +namespace { + +const lv_font_t *fontForScale(int scale) noexcept +{ + if (scale <= 1) return &lv_font_montserrat_10; + if (scale == 2) return &lv_font_montserrat_14; + if (scale == 3) return &lv_font_montserrat_20; + return &lv_font_montserrat_28; +} + +bool glyphDescriptor(const lv_font_t *font, unsigned char letter, + unsigned char next, lv_font_glyph_dsc_t &glyph) noexcept +{ + glyph = {}; + if (!font->get_glyph_dsc(font, &glyph, letter, next)) return false; + glyph.resolved_font = font; + return true; +} + +std::uint8_t alphaAt(const std::uint8_t *bitmap, + const lv_font_glyph_dsc_t &glyph, int x, int y) noexcept +{ + const unsigned bits = static_cast(glyph.format); + if (bits != 1 && bits != 2 && bits != 4 && bits != 8) return 0; + const std::size_t bit = glyph.stride != 0 + ? static_cast(y) * glyph.stride * 8U + x * bits + : (static_cast(y) * glyph.box_w + x) * bits; + const unsigned shift = 8U - bits - static_cast(bit & 7U); + const unsigned mask = (1U << bits) - 1U; + const unsigned sample = (bitmap[bit / 8U] >> shift) & mask; + return static_cast(sample * 255U / mask); +} + +std::uint16_t blend(std::uint16_t background, std::uint16_t foreground, + std::uint8_t alpha) noexcept +{ + const unsigned inverse = 255U - alpha; + const unsigned red = (((foreground >> 11) & 0x1fU) * alpha + + ((background >> 11) & 0x1fU) * inverse + 127U) / 255U; + const unsigned green = (((foreground >> 5) & 0x3fU) * alpha + + ((background >> 5) & 0x3fU) * inverse + 127U) / 255U; + const unsigned blue = ((foreground & 0x1fU) * alpha + + (background & 0x1fU) * inverse + 127U) / 255U; + return static_cast((red << 11) | (green << 5) | blue); +} + +} // namespace + +int textWidth(std::string_view value, int scale) noexcept +{ + const lv_font_t *font = fontForScale(scale); + int width = 0; + for (std::size_t index = 0; index < value.size(); ++index) { + lv_font_glyph_dsc_t glyph; + const auto letter = static_cast(value[index]); + const auto next = index + 1 < value.size() + ? static_cast(value[index + 1]) : 0; + if (glyphDescriptor(font, letter, next, glyph)) width += glyph.adv_w; + } + return width; +} + +void draw(std::span pixels, int canvasWidth, int canvasHeight, + std::string_view value, int x, int y, int scale, + std::uint16_t color) noexcept +{ + if (canvasWidth <= 0 || canvasHeight <= 0 || + pixels.size() < static_cast(canvasWidth * canvasHeight)) + return; + const lv_font_t *font = fontForScale(scale); + for (std::size_t index = 0; index < value.size(); ++index) { + lv_font_glyph_dsc_t glyph; + const auto letter = static_cast(value[index]); + const auto next = index + 1 < value.size() + ? static_cast(value[index + 1]) : 0; + if (!glyphDescriptor(font, letter, next, glyph)) continue; + glyph.req_raw_bitmap = 1; + const auto *bitmap = static_cast( + font->get_glyph_bitmap(&glyph, nullptr)); + const int glyphX = x + glyph.ofs_x; + const int glyphY = y + font->line_height - font->base_line - + glyph.box_h - glyph.ofs_y; + if (bitmap != nullptr) { + for (int row = 0; row < glyph.box_h; ++row) { + const int targetY = glyphY + row; + if (targetY < 0 || targetY >= canvasHeight) continue; + for (int column = 0; column < glyph.box_w; ++column) { + const int targetX = glyphX + column; + if (targetX < 0 || targetX >= canvasWidth) continue; + const std::uint8_t alpha = alphaAt(bitmap, glyph, column, row); + if (alpha == 0) continue; + auto &pixel = pixels[targetY * canvasWidth + targetX]; + pixel = blend(pixel, color, alpha); + } + } + } + x += glyph.adv_w; + } +} + +} // namespace buddy::font diff --git a/dependencies.lock b/dependencies.lock index a704b8f..70179f7 100644 --- a/dependencies.lock +++ b/dependencies.lock @@ -159,9 +159,17 @@ dependencies: source: type: idf version: 6.0.1 + lvgl/lvgl: + component_hash: 184e532558c1c45fefed631f3e235423d22582aafb4630f3e8885c35281a49ae + dependencies: [] + source: + registry_url: https://components.espressif.com/ + type: service + version: 9.5.0 direct_dependencies: - espressif/bmi270 - espressif/m5stack_core_s3_noglib -manifest_hash: 40a4aab284508c1dedc714650907563cf3c3f4f3be1df11f3241510dc1d7701e +- lvgl/lvgl +manifest_hash: 7828239a68b74b2b81a0c78591bf53f404f94dd833867143983e0d36145ad5e1 target: esp32s3 version: 2.0.0 diff --git a/docs/images/buddy-selector.png b/docs/images/buddy-selector.png index a2fbc59e4e87a7d62f43f2c4e326a84507fa4401..7ea6ae4de212771e4da0200ca85a240ec540782d 100644 GIT binary patch literal 10359 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

f@F4y`F_p%hS1~!`k@0 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 Mld+S2^=nqnyq9mddErf_3gdrF`dKX0RqJ${X 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 literal 2067 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 literal 1322 zcmeAS@N?(olHy`uVBq!ia0y~yU~~YoKd>+Z$!6vS5kN{I*(1o8fuTx`fuW&=f#DZW zsNn?zL#Y7+!>a@a2CEqi4B`cIb_Lo1CFBBpLR&pI$gO`ItQhGy?22e<~#5JNM zC9x#cD!C{XNHG{07@6rBSm+uWhZtB`0g;iZwt<0_fk8v|Q&toWx%nxXX_dG&d^C&Q z3Dlqgx1l66H?_DVF}DD>o@L54X$%a^Pd!~6Ln>~)z3aVck%0)?gNTrD%aHC@h2>qW zNBr^`QWn^x+8;8C~=V{!(M2F|?gY?tCZQPHZ&U^LWhg&=}wJ~p7H1mhsF$osD4QE46ty<-` zY35fJh5%$`}l)@b6)^%tuTJTa}<8S}3qAx-{fv}uMFXZeGV0$(3}?^j*cnk#MJ z5ar7@>(`~ZYWLiKJKmCd!`|jTNn|;En2ILD41`8Lw;90ZxlugsY4a86ux zHiJj*Oa9MXfB4sR-~7MH;rneLQ{|s?4s{xFdk8WxF5zon2x50&Sjnu=aD`F8;S~di zLMa2w1S8n1c*B@X$H-M3)_N1H(FYi}l5eObZ+kzQ_ z*>E^g)1msXZsZ)U%~yQpn~Q(Ea;9tF+K00?oH@>H`%=^(=6((@SIPXVx|_=u+|Rpz zY5PolVVln?^Bd33y2{wR?$X_y$h^JP^|yK7J(d1*St>p)^}wT0;p#fUDO^$^V&~^B z*_`(EK=tWu*8T7NQY+5xYPjflI)CzayFi!I{byZGH>I&U96|~*Szw6z6(yXLir5vs z@DuB_vu9IwGM)Zbw#TQjUs~$9PGhQp53hGZMfRUL$_6qq4aysO+R_s^Y+RXR3|Nji zw>)7u=V#;NdyxOcKDM3Ar>C85Xv@0#&fpZib!?2RXd_uuvr4f<_T!HH9BoQvyf}chAOKK|F{;&>14h>Z&EmyFJoVA zXLi7yOV<^SmS6aG?rp^TFAl*aEE!houVq!~mvJvHZC2m_+S~M#UFfOvUQzioOM!(1 NgQu&X%Q~loCIC4QCoup3 diff --git a/docs/images/claude-info.png b/docs/images/claude-info.png index 8729eb2d0631d351351854d88469a319c6a091e6..49007dd1c9cccf9c208944502b84c7ec8323f309 100644 GIT binary patch literal 8799 zcmdUVXHXPT*CsQJGDK$xgOV9?P>>u%V8|II=bS;3Q(khzY0&aBy&ll@w*4CHE#yR|_*wS|`hl9iTSxHt}+ZVKN^30xEuhp9;NRxq(?~imC zw`__OMXTmJ;u%X~;a1^$UP|4^myD;I7C(e0_WoGxS*Nq8r?Cp9FZJ!n{6M^3B+N>@ zN@>I;_M-Qw3F@Y7NwK_RSb)MM_{bm$;noj7@$H;+rPmlsuukM-qE*R-9lr~?{hj+a z(5~b6Ur+07KMs#Ib38NIP$*Una%;!-h%jC+W{i8?~i87_E6~DCf65H^gXhUR#XmS7IN@;CPUmu9D{cE4M(;LIFQ78k-EYdmm;OrsN$2vrPPJ z84}}+r{U`}c3SafJf(5mFpxO-=y%!jtQ_Wp&G0Ogn;G&LL8rg0*f!_2ew_Nbg6!Rf z>d9?*t1so;oYe6J#&b5cv4LU#rve$0NP($+Fko^!dx_s$+?#eX@S3?~VVx0<1J0{n zV~Ct9be97s=g`N?7ub!rY3Xx8A?C-|?fSP$!C$#>rAdcdN-oy5dcD8z8$Kfk&V?lN z(g@y{yDx<>j;{U+CGv&ZPLdGv0G+Q!Jmi>F-00I^DyJ_sR&#W*6i_Vr^E~@I6Hi|$ zj;v#lWF35Q;mg;qGfhEUfTM}Wk~wCdLPalR&$(HSN*K7IEtVhOrdpG&QczDq3imQzWK-~bZErIo9eWY*#TV{@xMhX}tzzGpb?#)291BF^iteiRKrG_scn?qGbx}2~@y8)JrmqkfuNSp$_MM;nOzduly??W=j zw>%Vu1fVGaYi1%dl3sc5u}bLOW*<5Auokr-J>uCiP8vQ!}B89fW9H$5dOuy$|nz&VHeT z9;-0{eh*2+!KV@E`KSQ_%Z5ToV8)rOU}+{PFkT|>J{cMZaCBTqdU34O1mOD8OsZG1 z3{nuqHJt7u2P8}Y%zSKzln)>N6F9(z{KIQ>7Qbyx`2uQ zFAP_DZcp)5e(>+!;$=g@drkzQue0j9-QJF!w_n*u(UZ?Vhpq}mvO->yZD_I!9N&sM z``#O^lQg)8h=6FI;%UXg_{!HrlXy2iGRV(32LoRexSSQ*Xid?fh4Mf0u5MdgCDdt1{joC&SRNj@N}%4R&K` zqs~Fi{ss6Yu4C4njdZFu!l^;c1T_Nr^^(^IZfAAmB>AD=ijr8|V?4HQVNd%l$gw5!bm?rdreWjpU*dg9L?U=8NEB=yX`b#6t8uS3`&r$_ zi=#s4^F@qx8eVVCPJI4zP1(@S=1+kZymPfK9&R+w6+P*Qfd))H`XME zE!4Or*k$+fgPZ=lRyTL_-TNh zyMt5Sz6(q6ilDm#IEehXQx5EuF&Cg8LO|TXI8f}L)FPC96)fn%fC%o-bU#vVdu4oM z8{KvEwH4OYutb^>)?rU|61ztYe-0;|sUj7FHM{L>dH5MuHlBfno^b_v)cRBCX$UZG zgODfpwu{r!_>W{B>s@70%Rw~!Ho3lk=(Dg-y0#Fw;^iVkHx$fieb6q5P3jHXgW%nH zm#*U;f=%7x zAUg0#wI@XrXGQWSy0!9!=v@0d?$l*G$P#8Md-&U=>w2z_l9kNg?>?>gL#|XWVb4UR z@^B=yoJSE4=T-ec4R$qJ!w%ue3uvdmOb%j zW5Qujr|L24$+P;3>!D+lESaRZ`PX7|#A?PjLn__ZQv-AUpa}D!%YiX+Tj3Ze`y@VU zzeBIA#s53`fLS8rofN|!9be0>AJ(?Q^~YxBlXhTPg60 z_eU}1D+6ZU$Pb3uf+eBa;0LUhNzD;X*KyPC8s*a6AMH-W}8XL z)WE}_1+B)-v`_WcSYk3FLqz`R{cH?|VzuvG>=&}vjN&U%kg6YvU|aepPrX-fc`uQ| zz2|)&!bPD~F&wH&ss03n+w}wu)h-rQPQ*&Vexjo~=pHfHAq`q#+*RTO&BymGf<+l+ zKiw>M-#ESI4ga<^##Z9$QZ>AI6HaoT%W3pR$6j>LanQ7`+r}6JRm&YWeA1_r#Li4o zdJRjKO}gbP_$d<44nd-E-q33#-de~Cbh`!od^YQz9CQ%=m;#+j<2~{5CTVh(Osy30 zrcWs_EZ7&-YS=f0l>tpFG*274J*rOcs_vVNP;OYS^-v{3l4DNE?J!BK%>MDkr?I-` z4;u2h-8~r%drJ;_(|auT)>i`IAY?5iyi_vsTYP)bxNKfOKIl;5VrA>d1qiG`BZH*X^RtGNQg7`p|@`Dj}7kgM&wmZpZdMo9l5Og?$|r zU63wkU0zJ~EV2)4p;tZg6#|vqSCi)A_f?gwD0$48?qAUf6MKde!_mr6U&Z{!(f4)K zi23-HC7W>l!v`+Rq(=)&bGlJebmit|;vC`Y41p|?oSz#Omb>>&{&MX=I0uEE%@5!& zaaj74+Kw`zy;BCx1i*Aq?D9ygZBC6Q1~i}arzK4{ z**-GvU=Z$|q6{Vlk--jOXkeAE_)D%XH>5@CTex)_bccP~NYVQj4ZFM9OFpg1FI@1d zBF4b_iD%hM4cSZg?A&(+F+ON65b07)3(`bi!VC>IJjrBJ700w)5}3+`{uiO};n9DFR- zdjWX}RXh&W*VedA-G|$A1B}M%$ks`= zOAn%GZnpm6F>*p50=(O3m-n+d)J4zT55BcI7vo;c=Z-B%G^x~P{Q&@(^_K7Moh|u^ z`D`ZgMao6`9w4JBt4$m1NUNsSl9-It$NN8!lP$s z6ix4Ku`XSGZcV#IMt{2LTBB${((tz>#!%;Hk>%(51iiAiSKL+7U~8sw1pL(2ByVX@~)(t8u66h z>=_H^4VVg5d*~6GwwHgP58*M;AEW$bM6DH#%+A+C6*VSpMXYOL&pszz8LYcFGl1w0 zG@X`Su=pKV1%27uN=P>%w;WXOQ`sx(`D=iI4u3JA^k9i-I$FhuH@Jj`oIL@bluWD# zG2(Z2SZO-o?kan%(rVuAt&xKd#;v|^zdKoZ9wbePP>H-+7(1w{s{MWM7?Dd`qwJyf zOxX0rv+1%U*tdd7`{c%7JZ4pKKx76SmvN_GXp)mE0Hv^RAFUu(n&Q-?X@R?&Kc;ug ziKe_c9+^|Megg4OK>}V^SWb@aSWy$Nw z)$ROooer;6jLWJ+;8nsGv#RQNd6DI3Gi(f$f^nzZ{~lCP;%HEP_4hfy<(hKhWi5%p zTItHHG)(2x-I>&%2fEiONQ(hA*YmGN+I=Q0&y7v)lgMc>&*^!b-h~JEDtLiq@?RUz z&L*ZCY1>IqtUc6Sbi@5shDbd_fOD8?l|v^roZG7XD~@#xm^242Y zcOgB%m{nn{y; zcuE}@&Wi^)>HE4oHrXzIpx16l9}Ue2$H8|V)1M$it~~&H`K>FZAS2SD5FtD6ZG8A4 zkal1n@aZyuHyNbBS^S7U5l~-w01}8EaKS*HK%g9bK@_-u4#(_Mc7WoH#m@0I7aeAT{b zR`W+wrYj}>(>qAop%WCgczBos#m0piw{pfW>qtT)ZAnF9ZYO}TxB#~CiRy*-r&*Xi z75bfI(_+y~+Kq!hVnqPlwBgn;E!*{Pb8|sOqR5GV`J;Tn=%<*_e=osPTxCz$yjTAK zUpC7H4$A!p_;=rK#z-;!1N>N_B_#MMuxnRD?4xkBJkosm-X-;^9BFd{9~BU3-At>Sos3s7s0`&BjTle zFCClDM!)5E-?s2{J|{R0E0umtx2@q#A5R;X|Nehgv+9G5tGVZ$S_tGsB~o-VixNo# zGj5g=GQwzX2mh;-wUfttx_-VX?{K^;GF)3}pg(GxPRGqoh}kv91Q(u<7tq4A-CZa> zI?90s=nW3uV9z%y7}o#n?^s$qq)B96RrEe%-ug9Wla}tr$QqFJFu0vTu)E2gIZRlj zMn7BZ8{uNOsP`UO`lDeBS2Hov#IZmzmgs`0c)DV9?{&0|=#k;q$S-=VHR{%B{N7BB zZ>1N3N{(!UX9!znIESFxjJ?HiA^xQ7SwQkxPY+NDlakU=;7WbTFnFDcRpQ(Nv2H$f znbR!RFAc-NH@-u2rq#UI#d7GvwfSUM^W_~mICZxji;{NqRpY$iEQkS{Kv1*%`Y1yKW>7;-H04&oqQb0k*CNK#WP}^ z$)-KkAlnVfp@E}V%>;b+%Tv|V)+ajnAo(@rA?>$JBi}D&$`hnAzf{|Hj+ZNji7gp? zOr`(c+NZ|HJoNFEwK8HqQ&P{lvhc@HzR1{SIVn+lC>_5@#fUrdlG{fN3 zm{rAfm~iw^E~7LbzL*k%c=0P;u+z)2U?VP_34ytJ;|izYSXs*Y)t&~h*hV>h3heyD z)p2u^!uAWdY0yb2hwVGmSA22%*e@i64f;~@Q3>V+L>AfjN<1Xej6cVb>D-3Lb`i8i zZBC%G(uvrmDUYw58G_hfmXU{CUi+Ct&XaJMsd_&0{Y<9RoY|uO*N8>N9v<9OCOAl(4szIYV&jR| z$l&1n!qr<6)hPceH{QnVY3lEmU-du%@g&>&KJj|7$)$eTLvBlHyW4Ns$^0641Y1; z^)>}@S=?FLQGF;jjEC5j&vUp!d8%Z$4kl0}qNIdkUd$a#wBZTuTITV~c2NV-Ot`I7 z4Uzz4Y(kDA+eGlgD8MRW9!D}zArPe!sYt|RAZ?65lUr>}d0oMePBwc3mCJ8br^!*XGLW)fjC zZz&JP&vF!_QCFyZ7>NptliuJ3W^R0&;fc>Jn0&@m)7xpP?-0XO?e%Qsg#7KKKQ`~= zi?m2+_D6F(n-dh7R<3F_rB86>Qb0lHd1Z=0U2wEyo^=z%td9N@S_Kx_pGXHQYSIKc^lX&^!@Ls2^DW)0LMI z32$CJ@wYtcYyxUH&rL+5IC;fAos?}sEa~cH8f+vU<*z%+^_4~sr~{$V;wIg76%NqS z;{D!|#--zE)%+^Pftqm4mAX=r3W_XU-+|f}db3$O2kqG&u zd_Ma(DBYA^^BhARam*^iS{NC*O>h)3F>|4v-7v{s1b{_LpFId9eqVLDt zk9;qBCZisfKW%jFD{)no+K76U0k2)sZ(;AyW@WN^ABT=Lr>qM!TLn@2$@aVsTHA=eo(qKZnHuIGRAJq_aD^Ix(BT1@SGk~KGbs4Q_(5ZI7A7sc{F0jju;8gXtCK`G_3E;@*IFk}cO=2Lq zr4oCh03lKUkR~v6n+{$IAdN9GZ4MJS3ZNs@Po-WaL$zf>A=ObN<8V+fz_y6#8Cn$} z5=%ZWHEb z+dv*q586LszZG74({D(}-$%a4R_hQ26~G0S{h+i#4Y@`JR?QCrzqXB$;)j_-UfkVv zegi#^0I-%UML-6Ui3SP+L^BJl@I!pSP&S7D^~cP$8d6h4aQnM}@%);0S+ZSh{|Id8 zhX=?BB8VwT>7=H;*{-iE)|}{6rD(JtkMN?zPth%=UO>#-{d#QQVaR>{!GY>MBK6hG zFt_XB40de4#>?yF3qr!PEj{|GxW827BC`QVVheWiFjSc~XC=kvuWygr8pce!T(-;| z9Qh8mGgiv~e$<6aw#Us6B z=QQQmLM2AG@Af{Y>v74nPHAP<<8>p#4_+!HrqRaPGgFcHSmuNUw6k5tj}yF0Rc$an znW=7|G|!t3Gf_(vkD7l(3B%5U?y&M%EmU}U{z<0n(!l&Ja628THTk`GLjC(}v-oMl z0F_CHn_t_HX$~))c|o%eG>Qbo9+pBn9o3XyyT4g$C|@SHM-CpDvB12UWsRb1==`o_ z4&?kT6-{13D3PpIJDGRy!2TgAOwY-uaKO~w(Q#R@qW~GP~z?Pg(^ZF+M zt*xdYh&OX($=W_1DbZ>Vy8Zi77zXz7j*6@(Pj1d*dc{uSet{Ir4;95Hm&SZZC-^ms#*w?W3;C5h`8d@8D2RyPxQvBmNX9(O)-s zVF1&H0I4*-j|i!UJ3;R|-*%Oa`4lUvPg~6pa%30M%jV4j0A>^vvCx?YS!v&K^Vn literal 1616 zcmZ`(2T;>z6#tSC5=KZ)mJFFPEFuvkc#%L*VhBS~lb(Pm1Omtg!xGSciKBo5GK3Z@ z5ezjbN+l`@!vL*-3=s%RI1!C94B#P(g3w&A_S(C8_wMcAy><68z4uWu%DTz`01VB| z^&kK+vRq$KK*%`u6P+Q4>>=;9mHPz}+W&eSLi-IKn}m31fOtU6tOA@ZQP| z6r1KnLCv5sXta)AO}wStWyYo)W(NndS;U}tmRtZ^ye+{RzteiV9UZ@uC?{KcD?FZv z$3qcP}cSonQw>#eN%8X_^)S08)kK>OxN*m0ju1 zuOQ&s>?u)oqqpfjnGuGN&nKmfko4}rx;HgaJ5<$s<&?K>;h9)$8KUyu?D}(ET)f@t zSTzAH#4&$R^SF>S)cG{4l$m97v_-6K9oU`Z7+djvMw)EWaOnCr$E#1?JX9_7-&7a) znQa>!`qH_c2|TZNx#lO#(q32HcWLs)zufIhK`+bF*1~)D2}a9uWA8?fTk}iMrx}mo zBFPIK@Ng?K(#8)umEvT@TXR~Gl<6&Ko+XuhD)2Gc(0_3d7CqNoZni9S!!)$`q>Vy(=?IYPus4vsinqwlf3T{8|GV+zsUMG*HhX z`~3E5htr#rRmdn!AVG5<*Sl8rPX@$m0-Gk05_b^8h@EoosvVENAGZZ$B!Xd?L3fRcx=;(8+8im}YNDyWiK%)MM$CkAah= zMbJ}a2)xn)uy$(z&+`KcMi1ZXVhU zmFdF{+oD4*Iv?@Zp08}+JNRX_3}&|!p%8&rKx9ntt;nkL(78}FF-QX#ZzN5AcTZ9 z^|A#yAhC65-TU?2`Q*LgJXIOuUU(tCxrUZc@J(jU)xwouymQi}mE{0=`|{I(A$X<8^PZM(J!{$_s(xIcxp?BiUHI7rznf{T8^vY|pAmzxx7dP-hecL73%s+{*rW zwP0d#lBbF{+tD(kZI^+5`~+Nl%};#ogrt$L54NrVztp90PU^rmGO>Ek*bJx9$Mg8r zbGIyNo1K=e#$9tynOLmxYo;vJE6Q>hPxptlnf~wA#S&fJ6QwLe8gJ?0%6p|uez)uF;Si?zcH j^K^s@PQj%&=d)p4qSZOUWY~cGl>!=NpX=@20U3V+qOr8= diff --git a/docs/images/claude-owl.png b/docs/images/claude-owl.png index 99b8edf502643a12cab80a64f96f47c9c04d16fb..41baa758f159710be50d03ce7ff64c1a0f5dfa84 100644 GIT binary patch literal 7123 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 E-;hrmmjD0& diff --git a/docs/images/codex-agents.png b/docs/images/codex-agents.png index 0bb56d747b01fbbf9e9be27b572e175b1cf41e82..6b25bd6b4737408c1564c2779f17924f97367ce2 100644 GIT binary patch literal 13314 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 VD!b$0^zZ*SvXV*?6=KGL{{zDD9&G>s literal 2499 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 literal 1771 zcmZ{kX;9PG8ph8*5cnI!1Px0egjB_wuxLOG5kd(Pgn(iUAfSl^B6yX6ED^ynBo#^k zNrB69fr1xBL0Yd!sX?oTkQjlw6pBz4kpQxTfLaR3(zKoFv>!V2%z4jw&ivl<;W_hW zv;4gcQRXNBfFaX|!3F@LYxlFdI@&c|rz%8S2z(#*egMvH13;Vsz>?Myj{$I+48Q~r z0GCn#OyerPJ4n-BkXiczy)b4ZHuvQG{2G8U1bGBWJ~Bf0g~_{NJ2MBYS<%IB=f5J6 z3f?$u#lS?5wY9Zt_9HAU3q|;OGtg$=f#=$TzL2@!OMe13gyD}YZX^)3mbK6;M0hM* z80o@Kh}0I~NOq*yld1L&I|Ip77p)w3*^(LQ)NDzO0svXWWOxK7znHGP7yEKArnSyCG<4PglPI-;dY;ElJ+Wi! z!5K6$1f_^Ipm;CJ1C7xHa*wA#cbKHhzP7I+ET@_czg_>lEc8vmndN5}g@brE(VNH9 zbNrvXL`gpQeV)uq?7G0bj+XGkqiuTSfMP4Q^KO7MifVPxZ>>BS5A8z#=xis_Q zmv!@wFVf>V^&#J6e;A{*FGj`K80kUVg6PrQy^}Y(b~f)g0);Kw?aa!1xqKE)jdxo% z?vSmoxGDR|V2;W&x2Wb4Rwtr< zKBPF4G~cOn;ldNuFfFxmEzam#e}&}u@$+#F5_nmuRk^?&gRh=2G*+u64Zi|SV?LtWKoePHlk_?2lgNX>3@-81#w@*N1RQx%GtF` z{6FvoS-@z*yL#W!3r?YbDA*!TE1o4G(=vIcZ#~djD z@8RJwnF0p4_nJKn@^;l6DhLEzsQ^*(eRX-n?2%tgZlr!Q*M439asd3VhOFFfcXiQl z{%$ij-v0V4K0V^=b6-x*ym8v(M$k288s~T1B7b6O+*P&0Zi;_i$eD=HU7!C7ZBvE# zO&S>dAT9gyhtZKn-Hy^pKYq3D?DwUgFD69FeJp9M$n^Uy(Ht_2yQ@U1s8A#+iXNI{ z$*ad&1dN8cR#j&HWXh)VCzcKT8nF57m#P*cgp8Q;?vLKTwND1sBH97S%Ft}ZQ`YHT zgIh~~)#+|y)re(5k`hGP4zcsVTZz2cuqSMlFsGJ){&fqnv8x(wWUSO&&0qjWv~m8F z@%GVy553K620hJBRGb9#ef(z49d~5v*WK>N24blA96RhQ#*c1W5x&b?(4F+pw22)i zu52!})pUX1BO1V7W)fHMo`o#P_^*m<2IEHIXNTlc+q9dAS5{q>X>=jNn$;HO?Wr+J~%M2Cr+OiNRc$ghJ{hF;W_gUghY}WgG95cD0 zW%Xx9qN$p6?xQCCUI&E2kH3Akc@Njm@~#S*uA5&zow|vdoNbUfs+*cjp&!hVj-#L1 zsvkUEY|_z}*1g+dr(OY{K57|-?%T++O9JChDNMZL@qhkO9Gdq&Z|9 zAyJ3xlJ_vbL>pKKn~uaCMp4rgsx~jtvxdp2V`J{LI=X2s^kuppJ)pI6^%2r@NklbOua6VapPyMFwgT3gLXNc zjw5nqVEaQc)09_Uqjsg3tF&{{rud}U3fci=?`KUYvD%pqm|p&j>w9?Teg*vj=&k?& diff --git a/docs/images/codex-navigate.png b/docs/images/codex-navigate.png index 9bea339721c25cd06d229387b4807d2f5790a510..cff140cfbaa13b1b7e6c2a26b80380e65d7de8a2 100644 GIT binary patch literal 9039 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 E0LI;u-2eap literal 1690 zcmZ{lc~p~E7Qo+^okWC+g#b20StLqA02>j46D4eA3jxz80s$1EvILs273C|U)l!R8 z6c8dHBZ3r(<3fWK!^p0|AWLRYgo2=?U_(F=Eim7iKjxhIqxanN?tAyX-?``9`_8*( zctJjfL<=GSz|hZ^%Lf4BAbTAKjjSioRYJs2V}1F70A$+%0Oten0dc{v0gy5Q(24+X zD+RzzT+tH3MglgxfG{s(GL0Yc)w_3VfM85*jiI$p(~8~@?DxF8PIIy`F0Q`4h6rxo z1>`;yIS!vpp$N?|$3-rQk35EoMxurbK9&y=9yo8cN(UQY%v1x}91Du)8EIQMb z?&K22bag}2`Sa~erW=z9ADzPfACQ!CC?PKEuYq}1gaQHV|Cd3MkaYNKNpzCaLCk(E4kl3N{%HaNvx7pEWtvSUOo2K5qdBo2-mC zGhQw7mYNDyt5N*#TJC_E^CyS6vgF2VA`<~2@97XL6*7j8lHe!{ois(An*iR>zkqbo zwS{TNB3EfAN zEg$7WI@ZIqvo=zWRCSz9%q)qnEf{sz21EZUq{A;5t3)`Fcus|4Q@9JD3O`;-G0=^u zZSetu2s8?z{!v8QpF!XGgwpm0k3R|AC=@X>>H2;g#Jwi zOA!w6G`m2ki~ROO+uuRG%=z=Ec#c-?O<6^u`CD;|23!$GJNW%>Ea_fVpsd0zAO?C( zBBzHF-l6xy^&&D-bdW>l+!?gJ*tmbF5D#bX`zSu!)G+!p zYCQ1y$}A+9)6ULClIBrW?&G?hK>1sD3BGUgj|2&Q#szh$mcQ}-ll1w%3Dgq0k3XXnw4m2iRGoG~|Ws9CQNS<;lx}w%@+< z0T`qu1Z6v9or77YybY`!USe<)eS4TsAGXOFW-|8ZOC%yU+;I7Jf5bk_Lw=V20YAC}LJW ziHTtzn{Hb3^#qvl5fl|@Oo5e^Zda<;@|EtD{NdFt{$Q^$rX!^Pw79P?@ZpIs3LADU z@OA_ieai03{N>9&Bf)jy$qFaRI?YAfGo@PtC*rr&dE#Odww&h1Z7Zs&ulkV{?cArJ zjpWhcr#W0s2ru<6xOhyNNR-=$Wevbb5+2s}%)(xwV#fykrJFffFVK(6g2Yjl(BUkM z^2(h`bDqXDQegeaa{GCWN63tu*}}+?TcU7^V!f%kV*Mp>b-DK^0sfgj5suhCeELe( zWy7oUp}jv&kTX(Aw&mhi6ZHaiWE2(o{RjsF*U*&#{wsCjN@TbI;O7;@y|Gu6_aF1- B(oX;Y diff --git a/main/idf_component.yml b/main/idf_component.yml index fdf75a0..fd0baea 100644 --- a/main/idf_component.yml +++ b/main/idf_component.yml @@ -1,3 +1,4 @@ dependencies: espressif/m5stack_core_s3_noglib: "3.0.2" espressif/bmi270: "1.1.0" + lvgl/lvgl: "^9.5.0" diff --git a/scripts/render_ui_previews.sh b/scripts/render_ui_previews.sh new file mode 100644 index 0000000..227e004 --- /dev/null +++ b/scripts/render_ui_previews.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -eu + +script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +project_dir=$(CDPATH= cd -- "$script_dir/.." && pwd) +work_dir=$(mktemp -d "${TMPDIR:-/tmp}/codex-buddy-previews.XXXXXX") +trap 'rm -rf "$work_dir"' EXIT HUP INT TERM + +cmake -S "$project_dir/test/host/ui_preview" -B "$work_dir/build" \ + -DCMAKE_BUILD_TYPE=Release +cmake --build "$work_dir/build" --target render_buddy_ui --parallel +"$work_dir/build/render_buddy_ui" "$work_dir" + +for source in "$work_dir"/*.ppm; do + name=$(basename "$source" .ppm) + sips -s format png "$source" \ + --out "$project_dir/docs/images/$name.png" >/dev/null +done diff --git a/sdkconfig.defaults b/sdkconfig.defaults index d2ec885..52ee7b1 100644 --- a/sdkconfig.defaults +++ b/sdkconfig.defaults @@ -12,3 +12,11 @@ CONFIG_SPIRAM_USE_MALLOC=y # The control-plane JSON parser uses a fixed token table while handling the # multi-report lighting configuration sent at connection time. CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192 + +# The framebuffer renderers use LVGL's anti-aliased Montserrat glyph data +# without requiring the LVGL object/display lifecycle. +CONFIG_LV_CONF_MINIMAL=y +CONFIG_LV_FONT_MONTSERRAT_10=y +CONFIG_LV_FONT_MONTSERRAT_14=y +CONFIG_LV_FONT_MONTSERRAT_20=y +CONFIG_LV_FONT_MONTSERRAT_28=y diff --git a/test/host/test_codex_ui.cpp b/test/host/test_codex_ui.cpp index 45c8876..85f4995 100644 --- a/test/host/test_codex_ui.cpp +++ b/test/host/test_codex_ui.cpp @@ -90,15 +90,15 @@ int main(void) Event event = ConnectionChanged{Connection::Connected}; assert(applyEvent(model, event)); buddy::codex::ui::render(model, nullptr, 0, storage); - assert(pixel_at(pixels, 160, 82) == rgb565(45, 145, 235)); + assert(pixel_at(pixels, 160, 82) == rgb565(75, 135, 188)); event = SlotSelected{1}; assert(applyEvent(model, event)); buddy::codex::ui::render(model, nullptr, 0, storage); assert(checksum(pixels) != disconnected); - assert(pixel_at(pixels, 55, 38) == rgb565(45, 190, 225)); - assert(pixel_at(pixels, 160, 38) == rgb565(35, 190, 100)); - assert(pixel_at(pixels, 265, 38) == rgb565(230, 65, 75)); + assert(pixel_at(pixels, 55, 40) == rgb565(73, 153, 174)); + assert(pixel_at(pixels, 160, 40) == rgb565(67, 146, 101)); + assert(pixel_at(pixels, 265, 40) == rgb565(181, 83, 89)); write_snapshot(std::getenv("UI_SNAPSHOT_DIR"), "control", pixels); event = PageSelected{Page::Navigate}; @@ -124,12 +124,12 @@ int main(void) dismiss_overlay(model); buddy::codex::ui::render(model, nullptr, 0, storage); - assert(pixel_at(pixels, 60, 45) == rgb565(15, 42, 65)); - assert(pixel_at(pixels, 165, 45) == rgb565(13, 48, 62)); - assert(pixel_at(pixels, 270, 45) == rgb565(12, 47, 31)); - assert(pixel_at(pixels, 60, 129) == rgb565(55, 39, 17)); - assert(pixel_at(pixels, 165, 129) == rgb565(54, 22, 29)); - assert(pixel_at(pixels, 190, 38) == rgb565(242, 246, 252)); + assert(pixel_at(pixels, 60, 45) == rgb565(23, 36, 48)); + assert(pixel_at(pixels, 165, 45) == rgb565(22, 39, 46)); + assert(pixel_at(pixels, 270, 45) == rgb565(22, 38, 30)); + assert(pixel_at(pixels, 60, 129) == rgb565(43, 35, 23)); + assert(pixel_at(pixels, 165, 129) == rgb565(43, 27, 31)); + assert(pixel_at(pixels, 190, 38) == rgb565(199, 206, 214)); const uint64_t agents_dim = checksum(pixels); buddy::codex::ui::render(model, nullptr, 800, storage); assert(checksum(pixels) != agents_dim); @@ -138,7 +138,7 @@ int main(void) set_slot(model, 2, SlotStatus::Idle, false); set_slot(model, 2, SlotStatus::Complete, false); buddy::codex::ui::render(model, nullptr, 800, storage); - assert(pixel_at(pixels, 160, 144) == rgb565(35, 190, 100)); + assert(pixel_at(pixels, 160, 144) == rgb565(67, 146, 101)); write_snapshot(std::getenv("UI_SNAPSHOT_DIR"), "complete", pixels); puts("codex_ui host tests passed"); return 0; diff --git a/test/host/ui_preview/CMakeLists.txt b/test/host/ui_preview/CMakeLists.txt new file mode 100644 index 0000000..70c4b31 --- /dev/null +++ b/test/host/ui_preview/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.16) +project(codex_buddy_ui_preview LANGUAGES C CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +get_filename_component(PROJECT_ROOT "${CMAKE_CURRENT_LIST_DIR}/../../.." ABSOLUTE) +set(LVGL_ROOT "${PROJECT_ROOT}/managed_components/lvgl__lvgl") +if(NOT EXISTS "${LVGL_ROOT}/CMakeLists.txt") + message(FATAL_ERROR "LVGL is not installed; run pio run first") +endif() + +set(LV_BUILD_CONF_PATH "${CMAKE_CURRENT_LIST_DIR}/lv_conf.h" CACHE PATH "" FORCE) +set(CONFIG_LV_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) +set(CONFIG_LV_BUILD_DEMOS OFF CACHE BOOL "" FORCE) +set(CONFIG_LV_USE_THORVG_INTERNAL OFF CACHE BOOL "" FORCE) +add_subdirectory("${LVGL_ROOT}" lvgl) + +add_executable(render_buddy_ui + "${PROJECT_ROOT}/test/host/render_buddy_ui.cpp" + "${PROJECT_ROOT}/components/buddy_ui/buddy_ui.cpp" + "${PROJECT_ROOT}/components/claude_model/claude_model.cpp" + "${PROJECT_ROOT}/components/codex_ui/codex_ui.cpp" + "${PROJECT_ROOT}/components/codex_model/codex_model.cpp" + "${PROJECT_ROOT}/components/ui_font/ui_font.cpp") + +target_compile_definitions(render_buddy_ui PRIVATE BUDDY_USE_LVGL_FONT=1) +target_compile_options(render_buddy_ui PRIVATE -Wall -Wextra -Werror -pedantic) +target_include_directories(render_buddy_ui PRIVATE + "${PROJECT_ROOT}/components/buddy_ui/include" + "${PROJECT_ROOT}/components/claude_model/include" + "${PROJECT_ROOT}/components/codex_ui/include" + "${PROJECT_ROOT}/components/codex_input/include" + "${PROJECT_ROOT}/components/codex_model/include" + "${PROJECT_ROOT}/components/codex_protocol/include" + "${PROJECT_ROOT}/components/ui_font/include") +target_link_libraries(render_buddy_ui PRIVATE lvgl::lvgl) diff --git a/test/host/ui_preview/lv_conf.h b/test/host/ui_preview/lv_conf.h new file mode 100644 index 0000000..2adef59 --- /dev/null +++ b/test/host/ui_preview/lv_conf.h @@ -0,0 +1,19 @@ +#ifndef LV_CONF_H +#define LV_CONF_H + +#define LV_COLOR_DEPTH 16 +#define LV_USE_STDLIB_MALLOC LV_STDLIB_CLIB +#define LV_USE_STDLIB_STRING LV_STDLIB_CLIB +#define LV_USE_STDLIB_SPRINTF LV_STDLIB_CLIB +#define LV_USE_OS LV_OS_NONE + +#define LV_FONT_MONTSERRAT_10 1 +#define LV_FONT_MONTSERRAT_14 1 +#define LV_FONT_MONTSERRAT_20 1 +#define LV_FONT_MONTSERRAT_28 1 +#define LV_FONT_DEFAULT &lv_font_montserrat_14 + +#define LV_BUILD_EXAMPLES 0 +#define LV_BUILD_DEMOS 0 + +#endif