Add widget kit, input layer with Compose Key, Launcher and demo apps

Steps 5 and 6 of M0.
- lib/ui (host-tested): ListModel, LineEditor (UTF-8, byte-limited),
  wrapText, ToastQueue, DialogModel
- lib/input (host-tested): KeyMapper turns keyboard state into logical
  keys (Fn arrows, ` Back, Fn+` Home) and composes accents with opt
- src/ui: off-screen 16-bit frame pushed in one transfer (fixes the
  flicker), Status Bar, list, text, line editor, dialog, Toast widgets,
  Notifier (Toast + beep + LED), vendored public-domain Latin-1 fonts
- Apps: minimal Launcher, Widget demo, Diagnostics (SD erase now
  behind a confirmation dialog)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-02 10:35:48 +02:00
co-authored by Claude Opus 5.5
parent 36ebd23417
commit 879cb0d0b6
39 changed files with 2119 additions and 92 deletions
+94
View File
@@ -0,0 +1,94 @@
#include <unity.h>
#include "list_model.h"
using namespace roro;
void setUp() {}
void tearDown() {}
void test_empty_list_has_no_selection() {
ListModel l(5);
TEST_ASSERT_EQUAL(-1, l.selected());
l.down();
TEST_ASSERT_EQUAL(-1, l.selected());
}
void test_selection_starts_at_first_item() {
ListModel l(5);
l.setCount(3);
TEST_ASSERT_EQUAL(0, l.selected());
}
void test_down_and_up_move_the_selection() {
ListModel l(5);
l.setCount(3);
l.down();
TEST_ASSERT_EQUAL(1, l.selected());
l.up();
TEST_ASSERT_EQUAL(0, l.selected());
}
void test_selection_wraps_around_at_both_ends() {
ListModel l(5);
l.setCount(3);
l.up();
TEST_ASSERT_EQUAL(2, l.selected());
l.down();
TEST_ASSERT_EQUAL(0, l.selected());
}
void test_scrolls_to_keep_the_selection_visible() {
ListModel l(3);
l.setCount(10);
for (int i = 0; i < 4; i++) l.down(); // selected 4
TEST_ASSERT_EQUAL(4, l.selected());
TEST_ASSERT_EQUAL(2, l.firstVisible()); // rows 2,3,4
for (int i = 0; i < 3; i++) l.up(); // selected 1
TEST_ASSERT_EQUAL(1, l.firstVisible());
}
void test_wrapping_to_the_end_scrolls_to_the_last_page() {
ListModel l(3);
l.setCount(10);
l.up();
TEST_ASSERT_EQUAL(9, l.selected());
TEST_ASSERT_EQUAL(7, l.firstVisible());
}
void test_shrinking_the_list_keeps_selection_in_range() {
ListModel l(3);
l.setCount(10);
l.up(); // 9
l.setCount(4);
TEST_ASSERT_EQUAL(3, l.selected());
TEST_ASSERT_EQUAL(1, l.firstVisible());
l.setCount(0);
TEST_ASSERT_EQUAL(-1, l.selected());
}
void test_page_moves_by_visible_rows_without_wrapping() {
ListModel l(3);
l.setCount(10);
l.pageDown();
TEST_ASSERT_EQUAL(3, l.selected());
l.pageDown();
l.pageDown();
l.pageDown();
TEST_ASSERT_EQUAL(9, l.selected());
l.pageUp();
TEST_ASSERT_EQUAL(6, l.selected());
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_empty_list_has_no_selection);
RUN_TEST(test_selection_starts_at_first_item);
RUN_TEST(test_down_and_up_move_the_selection);
RUN_TEST(test_selection_wraps_around_at_both_ends);
RUN_TEST(test_scrolls_to_keep_the_selection_visible);
RUN_TEST(test_wrapping_to_the_end_scrolls_to_the_last_page);
RUN_TEST(test_shrinking_the_list_keeps_selection_in_range);
RUN_TEST(test_page_moves_by_visible_rows_without_wrapping);
return UNITY_END();
}