Skip to content

Focus: defer focus border until first directional navigation#173

Merged
ThomasFarstrike merged 1 commit into
MicroPythonOS:mainfrom
bitcoin3us:fix/touch-aware-focus-border
Jun 26, 2026
Merged

Focus: defer focus border until first directional navigation#173
ThomasFarstrike merged 1 commit into
MicroPythonOS:mainfrom
bitcoin3us:fix/touch-aware-focus-border

Conversation

@bitcoin3us

Copy link
Copy Markdown
Contributor

Make add_focus_border() (mpos/ui/focus.py) skip the focus ring on pointer/touch devices.

Why

add_focus_border() draws a focus highlight and adds the widget to the default focus group — a navigation aid for keypad/encoder devices, where it shows which widget the moving highlight is on. On a pointer/touch UI there is no moving highlight: the ring only appears after a tap and then lingers around the tapped widget.

Concretely, this is why the Lightning Piggy balance shows a gold rectangle after you tap it (the balance is add_focus_border-ed in 0.6.0, and tapping it cycles the denomination). On a touchscreen that ring is visual noise, not a cue.

What

def add_focus_border(widget, ...):
    from .input_manager import InputManager
    if InputManager.has_pointer():
        return
    ...
  • Reuses the existing InputManager.has_pointer() (no new detection).
  • Full no-op on touch: skips both the FOCUSED/DEFOCUSED border callbacks and the focus-group registration. Widgets remain interactive through their own CLICKABLE flag / click handlers, which are independent of the focus group.
  • Keypad/encoder devices are unchangedhas_pointer() is False there, so the border is added exactly as before.

Hybrid devices (both touch and a keypad) would also skip the ring; that edge case can be refined later if needed — flagging it explicitly rather than silently.

Testing

  • tests/test_focus_border_touch.py added: asserts add_focus_border() registers no callbacks when InputManager.has_pointer() is patched True.
  • Verified the logic in a CPython stub harness (lvgl + InputManager stubbed): touch present → 0 callbacks (no-op); no pointer → 2 callbacks registered.
  • Confirmed InputManager.has_pointer() returns True on a real touchscreen device (ESP32-S3), so the guard fires there.
  • ⚠️ I did not run ./tests/unittest.sh locally — it needs a freshly built desktop binary with this change and my local build is stale. Please run ./tests/unittest.sh tests/test_focus_border_touch.py in CI / on a current build.

Merge checklist

  • CHANGELOG: ✅ added under Future release → Frameworks
  • App version / MANIFEST: n/a (framework change)
  • MAINTAINERS.md / boards: n/a
  • Settings migration: n/a
  • Unit tests: ✅ added (no-op path); see testing caveat above
  • Frameworks docs (docs.micropythonos.com): not updated — add_focus_border behavior is now device-conditional; happy to add a note if there is a docs page for it.

@ThomasFarstrike

Copy link
Copy Markdown
Contributor

Would indeed be nice to find a solution for this and I guess it would work but indeed:

Hybrid devices (both touch and a keypad) would also skip the ring

...but I was thinking, how about on all devices, we still add the widget to the focusgroup, but don't do anything with the border (no set_style_border_color etc) until the user uses "move_focus_direction" , meaning they pushed the joystick?

So first they don't see anything special, but as soon as they press the joystick once the set_style_border_color , set_style_border_width , set_style_border_opa and set_style_radius are applied so it behaves as normal?

Per review feedback on MicroPythonOS#173: instead of skipping add_focus_border() on
touch devices (which mishandled hybrid touch+keypad devices), keep adding
every widget to the focus group but don't draw the border until the user
actually navigates by direction.

_focus_border_handler() is a no-op until enable_focus_borders() is called,
which move_focus_direction() does on the first joystick/keypad press:
- touch-only UIs (focus by tapping) never show a stray ring
- keypad/encoder devices show it the moment they navigate
- hybrid devices show it only once the joystick is actually used

- CHANGELOG: Frameworks entry
- tests/test_focus_border_deferred.py: no-border-until-nav unit test
@bitcoin3us bitcoin3us force-pushed the fix/touch-aware-focus-border branch from 7b8eafd to 0af5dcc Compare June 26, 2026 06:48
@bitcoin3us bitcoin3us changed the title Focus: make add_focus_border() a no-op on touchscreen devices Focus: defer focus border until first directional navigation Jun 26, 2026
@bitcoin3us

Copy link
Copy Markdown
Contributor Author

Good call — adopted your approach. Pushed a rewrite (force-update, rebased onto current main, so the CHANGELOG conflict is resolved).

Instead of skipping add_focus_border() on touch devices, every widget is still added to the focus group, but the border is now deferred until the first directional navigation:

  • mpos/ui/focus.py: module flag _focus_nav_active (starts False) + enable_focus_borders(). _focus_border_handler() early-returns while the flag is False, so no border is drawn.
  • mpos/ui/focus_direction.py: move_focus_direction() calls enable_focus_borders() at the top, so the first joystick/keypad press flips it on and the resulting FOCUSED event draws the border as normal.

Behavior:

  • touch-only (focus by tapping) → never shows a ring;
  • keypad/encoder → shows it the moment they navigate;
  • hybrid → shows it only once the joystick is actually used — which fixes the caveat you flagged.

add_focus_border itself is back to its original shape (no capability check), so it's a smaller change than my first version.

Testing: tests/test_focus_border_deferred.py asserts no border before nav and a border after enable_focus_borders(). Verified the logic in a CPython stub (both states pass); I did not run ./tests/unittest.sh locally (needs a fresh desktop build), so please run it in CI / on a current build.

@ThomasFarstrike ThomasFarstrike merged commit 8185b76 into MicroPythonOS:main Jun 26, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants