Skip to content

services/touch: synthesize button clicks from touch, and route action-bar taps#1785

Open
tsutomu1984-hub wants to merge 8 commits into
coredevices:mainfrom
tsutomu1984-hub:experimental/touch-click-synthesis
Open

services/touch: synthesize button clicks from touch, and route action-bar taps#1785
tsutomu1984-hub wants to merge 8 commits into
coredevices:mainfrom
tsutomu1984-hub:experimental/touch-click-synthesis

Conversation

@tsutomu1984-hub

Copy link
Copy Markdown

Depends on: #1784 (touch_service list_node fix — required for correct subscriber-count arbitration).

What

A kernel-side touch-to-click synthesis bridge: it maps touch gestures to the existing button semantics of whatever app/modal is focused, so apps that use their own click handlers (rather than ScrollLayer/MenuLayer) respond to touch:

  • tap → SELECT (or UP/SELECT/DOWN by vertical zone when the tap lands on an ActionBarLayer, e.g. the Music app's volume / play-pause bar)
  • vertical swipe → UP / DOWN

How it differs from #1773 (design note)

Unlike #1773, which changes the applib ScrollLayer/MenuLayer widgets that third-party apps link against, this bridge is a kernel-side input mapping with no applib/ABI change. It does not modify any widget; it posts the same PEBBLE_BUTTON_DOWN/PEBBLE_BUTTON_UP events a physical press would, which flow through the normal click path. So it introduces no new per-widget behavior and no SDK surface.

Gating & arbitration

  • Build-time: behind CONFIG_TOUCH_CLICK_SYNTHESIS (Kconfig, default n) — not compiled into default builds.
  • Runtime: only runs while the global touch service is enabled (persisted touch_enabled pref, default on) and the foreground is a non-watchface app or a focused modal.
  • Arbitration: suppressed when the focused window consumes touch itself (touch_has_app_subscribers()), so it never double-handles apps that already use ScrollLayer/MenuLayer touch (applib: touch drag-to-scroll and tap-to-select for ScrollLayer / MenuLayer #1773) or their own touch subscription.

Note on third-party apps (re: the #1773 review discussion)

When the Kconfig is enabled, the bridge does map touch onto the existing button handlers of the focused app, including third-party apps that have no touch handling of their own — giving them touch as an alternate way to trigger their current button actions (never new behavior, and never for apps that already handle touch). There is no per-app / system-app-only restriction today. If maintainers prefer limiting synthesis to system apps or adding a dedicated runtime toggle, that's a straightforward follow-up and we're happy to add it.

Testing

Unit tests: test_touch_click_synth (23) + test_touch_click_arbitration. Verified on-device (Pebble Time 2): tap→SELECT and swipe on Notifications; ActionBar up/select/down taps in Music; no misfires on the watchface.

🤖 Generated with Claude Code

tsutomu1984-hub and others added 8 commits July 22, 2026 11:48
Investigation-only test (no synthesis bridge is implemented) for a
possible future touch->button "synthesis bridge" that would let apps
using their own click handlers respond to touch. It models the proposed
suppression decision -- synthesize a button only when the focused app is
not a direct touch subscriber -- against the real
touch_has_app_subscribers() bookkeeping plus a dummy touch-consuming
app, and asserts that a single touch never produces a double action.

It also makes the async-subscription timing window observable: with a
persistent-sensor bridge, a touch in the window between an app calling
subscribe() and KernelMain processing it would be wrongly synthesized,
whereas the coupled-sensor design is race-free.

Kept on experimental/touch-click-synthesis for a separate future PR;
the synthesis bridge itself is intentionally not implemented yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
Add touch_set_synthesis_enabled(), a kernel-internal holder of the touch
sensor for the upcoming touch->click synthesis bridge, mirroring the
backlight subscription. Like the backlight holder it powers the sensor
but is excluded from touch_has_app_subscribers(), so it never counts as
a real app subscriber.

Also add the CONFIG_TOUCH_CLICK_SYNTHESIS Kconfig option (default n) that
will gate the bridge, kept separate from CONFIG_TOUCH so the feature can
be disabled independently of the touch driver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
Add a kernel-level bridge (CONFIG_TOUCH_CLICK_SYNTHESIS) that turns a
touch tap into a SELECT button click, so apps that use their own click
handlers instead of ScrollLayer/MenuLayer respond to touch.

The bridge is active only while a watchapp is the focused foreground
process (gated on PEBBLE_APP_DID_CHANGE_FOCUS_EVENT), giving it the same
"on while viewing an app" sensor power profile as the widget-level touch
support. Tap detection reuses ScrollLayer's definition: a liftoff within
300ms that never moved past 10px. On a tap it posts a SELECT button
down+up through the normal button path, and it suppresses itself while
the focused app consumes touch directly (touch_has_app_subscribers()) so
there is no double action.

Phase 1 handles tap->SELECT only; swipe->UP/DOWN/BACK is a follow-up.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
Extend the touch->click synthesis bridge so a vertical swipe (a drag past
the 10px threshold whose net movement is predominantly vertical)
synthesizes an UP or DOWN button click, alongside the existing
tap->SELECT. Horizontal swipes are ignored for now; BACK is a later
phase.

The mapping is spatial: a swipe toward the top of the screen is UP,
toward the bottom is DOWN. The foreground gate and the
touch_has_app_subscribers() arbitration are shared with the tap path, so
a swipe is also suppressed while the focused app consumes touch itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
Extend the touch-to-click bridge so it is active while a focusing modal
(notification popup, action menu, dialog, ...) is up, not only while a
watchapp is the focused foreground process. A focused modal receives
button events in launcher_handle_button_event, so synthesizing for it
lets touch drive, e.g., scrolling the notification detail body.

The synthesis decision is now a live re-check evaluated at touch time
rather than a value cached from the focus event. A focusing modal's
did_focus fires only once its entrance transition completes, so a cached
gate could miss touches arriving before then; the live re-check is robust
to that timing as long as the sensor is held. The focus event still
drives the sensor hold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
Invert the vertical-swipe to UP/DOWN mapping so it follows the
content-scroll convention used by ScrollLayer/MenuLayer touch and common
touch UIs: flicking the finger up (to reveal content further down)
synthesizes DOWN, and flicking down synthesizes UP. Previously the button
matched the finger's physical travel, which felt reversed when scrolling
a notification body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
Add a kernel-visible action bar descriptor {present, global frame, icon
mask} to the touch-to-click synthesis bridge, published from applib via a
new internal syscall (sys_touch_click_synth_set_action_bar). When a tap
lands on the bar, the bridge routes it to UP/SELECT/DOWN by vertical zone
(top/middle/bottom third), falling back to SELECT for a zone whose icon
is absent; taps outside the bar remain SELECT. This lets apps that draw
an ActionBarLayer and use their own click handlers (e.g. the Music app's
volume-up / play-pause / volume-down bar) respond to touch.

Swipes are unchanged (full-screen UP/DOWN). The descriptor is dropped
when the bridge leaves the foreground so it can't route taps in the next
context.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
Publish the action bar's geometry and icon-presence mask to the
touch-to-click synthesis bridge on add-to-window, icon change, and
remove-from-window, so a tap on the bar is routed to the matching button.
All of it compiles out unless CONFIG_TOUCH_CLICK_SYNTHESIS is set, so the
app SDK build is unaffected.

Also clear the descriptor from the Music app's deinit: it does not
otherwise remove its action bar on exit, which would leave a stale
descriptor able to route right-edge taps in the next app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: tsutomu sasaki <tsutomu1984@gmail.com>
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.

1 participant