Arrow keys work without Fn outside Text Entry

; . , / are arrows on their own unless the foreground App is editing
text (App::textEntryActive); Fn + those keys are arrows everywhere.

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 13:59:49 +02:00
co-authored by Claude Opus 5.5
parent 51698a8011
commit 5fd351c45e
10 changed files with 82 additions and 6 deletions
+4
View File
@@ -66,6 +66,10 @@ The regulatory band plan the device transmits under (here EU868). It sets the al
**Duty Cycle Budget**:
The share of airtime the Region allows this device to transmit. When it's used up, outgoing traffic waits.
**Text Entry**:
When an App is editing text. During Text Entry, `;` `.` `,` `/` type their characters and Fn makes them arrows. Otherwise they are arrows on their own.
_Avoid_: edit mode, insert mode
**Compose Key**:
The `opt` key used as a dead key. Pressing it and then a base letter types an accented character (e.g. `opt` `'` `e` → é).
_Avoid_: modifier, alt
+3
View File
@@ -22,6 +22,9 @@ class App {
return false;
}
// True while the App is editing text: the arrow keys then type ; . , / and need Fn to move.
virtual bool textEntryActive() const { return false; }
// Called every main-loop pass while in the foreground (e.g. to refresh live values).
virtual void update(uint32_t nowMs) { (void)nowMs; }
+10 -3
View File
@@ -87,14 +87,21 @@ void KeyMapper::onChar(char c, const RawKeys& keys, std::vector<KeyEvent>& out)
return;
}
if (keys.fn) {
if (keys.fn || !textEntry_) {
switch (c) {
case ';': out.push_back(KeyEvent::of(Key::Up)); return;
case '.': out.push_back(KeyEvent::of(Key::Down)); return;
case ',': out.push_back(KeyEvent::of(Key::Left)); return;
case '/': out.push_back(KeyEvent::of(Key::Right)); return;
case '`': out.push_back(KeyEvent::of(Key::Home)); return;
default: return; // other Fn combos are unassigned
case '`':
if (keys.fn) {
out.push_back(KeyEvent::of(Key::Home));
return;
}
break;
default:
if (keys.fn) return; // other Fn combos are unassigned
break;
}
}
if (c == '`') {
+8 -2
View File
@@ -22,12 +22,17 @@ struct RawKeys {
};
// Turns keyboard state changes into logical KeyEvents: only newly pressed keys produce events;
// Fn + ; . , / are arrows; ` is Back and Fn + ` is Home; the Compose Key (opt) followed by an
// accent and a letter types the accented letter (opt ' e -> é).
// Fn + ; . , / are arrows, and so are ; . , / alone when no text is being entered; ` is Back and
// Fn + ` is Home; the Compose Key (opt) followed by an accent and a letter types the accented
// letter (opt ' e -> é).
class KeyMapper {
public:
std::vector<KeyEvent> update(const RawKeys& keys);
// Whether the foreground App is editing text (default). Outside text entry, the arrow keys
// don't need Fn.
void setTextEntry(bool active) { textEntry_ = active; }
// 0 when idle, '*' when opt was pressed, or the accent character waiting for its letter.
char pendingCompose() const { return compose_; }
@@ -36,6 +41,7 @@ class KeyMapper {
RawKeys previous_;
char compose_ = 0;
bool textEntry_ = true;
};
} // namespace roro
+1 -1
View File
@@ -11,7 +11,7 @@ const char* const kText =
"Bienvenue sur roro9stack ! Ce texte teste le retour \xC3\xA0 la ligne automatique et les "
"accents : \xC3\xA9t\xC3\xA9, \xC3\xA0 bient\xC3\xB4t, gar\xC3\xA7on, No\xC3\xABl, "
"ma\xC3\xAEtre, o\xC3\xB9.\n\n"
"Use Fn+; and Fn+. to scroll, ` to go back. Fn+` always returns to the Launcher.\n\n"
"Use ; and . to scroll, ` to go back. Fn+` always returns to the Launcher.\n\n"
"Unbreakablewordthatislongerthanawholelineofthescreen to test hard breaks.";
} // namespace
+1
View File
@@ -19,6 +19,7 @@ class DemoApp : public App {
explicit DemoApp(EventBus& bus) : bus_(bus) {}
void onEnter() override;
bool onKey(const KeyEvent& e) override;
bool textEntryActive() const override { return page_ == Page::Editor && !dialog_; }
void draw(Canvas& c) override;
private:
+1
View File
@@ -34,6 +34,7 @@ class SettingsApp : public App {
void onEnter() override;
bool onKey(const KeyEvent& e) override;
void update(uint32_t nowMs) override;
bool textEntryActive() const override { return page_ == Page::Text; }
void draw(Canvas& c) override;
private:
+3
View File
@@ -13,6 +13,9 @@ class SetupApp : public App {
public:
SetupApp(Settings& settings, AppManager& apps);
bool onKey(const KeyEvent& e) override;
bool textEntryActive() const override {
return wizard_.step() == SetupWizard::Step::LongName || wizard_.step() == SetupWizard::Step::ShortName;
}
void draw(Canvas& c) override;
private:
+1
View File
@@ -143,6 +143,7 @@ void loop() {
M5Cardputer.update();
if (M5Cardputer.Keyboard.isChange()) {
bool swallow = M5Cardputer.Keyboard.isPressed() && power->onKey(now); // only woke the screen
keyMapper.setTextEntry(apps->foreground().textEntryActive());
auto events = keyMapper.update(readKeys()); // always, so held keys are tracked
if (!swallow)
for (auto& e : events) apps->handleKey(e);
+50
View File
@@ -175,6 +175,51 @@ void test_compose_state_is_visible_for_the_ui() {
TEST_ASSERT_EQUAL('^', m.pendingCompose());
}
void test_arrow_keys_work_without_fn_when_not_typing() {
KeyMapper m;
m.setTextEntry(false);
TEST_ASSERT_EQUAL(static_cast<int>(Key::Up), static_cast<int>(press(m, chars({';'}))[0].key));
release(m);
TEST_ASSERT_EQUAL(static_cast<int>(Key::Down), static_cast<int>(press(m, chars({'.'}))[0].key));
release(m);
TEST_ASSERT_EQUAL(static_cast<int>(Key::Left), static_cast<int>(press(m, chars({','}))[0].key));
release(m);
TEST_ASSERT_EQUAL(static_cast<int>(Key::Right), static_cast<int>(press(m, chars({'/'}))[0].key));
}
void test_arrow_keys_type_characters_during_text_entry() {
KeyMapper m;
m.setTextEntry(true);
auto ev = press(m, chars({';'}));
TEST_ASSERT_EQUAL(static_cast<int>(Key::Char), static_cast<int>(ev[0].key));
TEST_ASSERT_EQUAL(';', ev[0].ch);
}
void test_fn_arrows_work_in_both_modes() {
KeyMapper m;
m.setTextEntry(false);
TEST_ASSERT_EQUAL(static_cast<int>(Key::Up), static_cast<int>(press(m, withFn({';'}))[0].key));
release(m);
m.setTextEntry(true);
TEST_ASSERT_EQUAL(static_cast<int>(Key::Up), static_cast<int>(press(m, withFn({';'}))[0].key));
}
void test_other_characters_still_type_when_not_typing() {
KeyMapper m;
m.setTextEntry(false);
auto ev = press(m, chars({'w'}));
TEST_ASSERT_EQUAL('w', ev[0].ch);
}
void test_shifted_arrow_keys_type_their_symbols_when_not_typing() {
// Shift+; is ':' — the driver reports the shifted character, which isn't an arrow.
KeyMapper m;
m.setTextEntry(false);
RawKeys r = chars({':'});
r.shift = true;
TEST_ASSERT_EQUAL(':', press(m, r)[0].ch);
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_plain_character);
@@ -191,5 +236,10 @@ int main() {
RUN_TEST(test_opt_followed_by_a_non_accent_types_it_normally);
RUN_TEST(test_pressing_opt_twice_cancels);
RUN_TEST(test_compose_state_is_visible_for_the_ui);
RUN_TEST(test_arrow_keys_work_without_fn_when_not_typing);
RUN_TEST(test_arrow_keys_type_characters_during_text_entry);
RUN_TEST(test_fn_arrows_work_in_both_modes);
RUN_TEST(test_other_characters_still_type_when_not_typing);
RUN_TEST(test_shifted_arrow_keys_type_their_symbols_when_not_typing);
return UNITY_END();
}