Skip to content

Commit b3e0219

Browse files
jplexerclaude
andcommitted
fw/applib/ui: Open plain menu rows on a single tap
A tap on a not-selected row of a plain (non-center-focused) menu used to only select it, so opening an item after a scroll took two taps. Make plain menus behave like a phone list instead: a single tap selects the row (selection_will_change honoured) and activates it in the same gesture. A veto still changes nothing, and a redirect only selects the redirected row - opening a row the finger never touched would be a misfire. The commit uses MenuRowAlignNone so the content does not shift as the activated window pushes. Center-focused carousels keep the two-step model (tap centres, tap on the centred row activates); the double-tap window machinery is now carousel-only, since plain menus activate on the first tap. Short-item action menus keep the two-step tap via the new internal menu_layer_set_tap_select_only(): their rows hold several columns and the row-granular tap hit-test cannot tell which one the finger meant. Fixes FIRM-4102 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Joshua Jun <lets@throw.rocks>
1 parent 5055310 commit b3e0219

5 files changed

Lines changed: 153 additions & 126 deletions

File tree

src/fw/applib/ui/action_menu_layer.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,10 @@ static void prv_update_aml_cache(ActionMenuLayer *aml, int selected_index) {
746746
const bool center_focused = !prv_aml_is_short(aml);
747747
menu_layer_set_center_focused(&aml->menu_layer, center_focused);
748748
#endif
749+
// Short-item rows hold several columns, and the row-granular tap hit-test cannot tell which
750+
// column the finger meant — keep the two-step tap (select, then activate) for those instead of
751+
// the plain menus' tap-to-activate.
752+
menu_layer_set_tap_select_only(&aml->menu_layer, prv_aml_is_short(aml));
749753

750754
layer_mark_dirty(&aml->layer);
751755
menu_layer_reload_data(&aml->menu_layer);

src/fw/applib/ui/menu_layer.c

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1495,6 +1495,13 @@ void menu_layer_set_center_focused(MenuLayer *menu_layer, bool center_focused) {
14951495
menu_layer_update_caches(menu_layer);
14961496
}
14971497

1498+
void menu_layer_set_tap_select_only(MenuLayer *menu_layer, bool tap_select_only) {
1499+
if (!menu_layer) {
1500+
return;
1501+
}
1502+
menu_layer->tap_select_only = tap_select_only;
1503+
}
1504+
14981505
bool menu_layer_get_scroll_wrap_around(MenuLayer *menu_layer) {
14991506
return menu_layer->scroll_wrap_around;
15001507
}
@@ -1550,13 +1557,19 @@ void menu_layer_set_scroll_vibe_on_blocked(MenuLayer *menu_layer, bool scroll_vi
15501557
// public per-menu gesture surface (and the unit-test entry points).
15511558
//
15521559
// Live scrolling happens on pan Updated. On a plain (non-center-focused) menu the selection is
1553-
// frozen for the whole pan and never changes on liftoff — a pan scrolls, a tap selects. A
1554-
// center-focused menu is a carousel instead: the focus stays pinned at the viewport centre, so the
1555-
// row crossing the centre becomes the selection live during the pan (through the full
1556-
// selection_will_change contract), and liftoff settles the selected row to the exact centre.
1557-
1558-
// Two independent taps within this window on the same menu count as a "double tap" and activate the
1559-
// last tap-selected row (there is no dedicated double-tap recognizer). CALIBRATION: ~300ms is the
1560+
// frozen for the whole pan and never changes on liftoff — a pan only scrolls. A center-focused
1561+
// menu is a carousel instead: the focus stays pinned at the viewport centre, so the row crossing
1562+
// the centre becomes the selection live during the pan (through the full selection_will_change
1563+
// contract), and liftoff settles the selected row to the exact centre.
1564+
//
1565+
// Taps differ by shape. A plain menu behaves like a phone list: a single tap on any row both
1566+
// selects it (selection_will_change honoured) and activates it — one tap opens the item. A
1567+
// carousel keeps the two-step model (a tap on an off-centre row centres it, a tap on the centred
1568+
// row activates), bridged by the double-tap window below.
1569+
1570+
// Two independent taps within this window on the same carousel count as a "double tap" and activate
1571+
// the last tap-selected row (there is no dedicated double-tap recognizer). Plain menus activate on
1572+
// the first tap, so the window only ever arms on center-focused menus. CALIBRATION: ~300ms is the
15601573
// usual comfortable double-tap spacing; tune on hardware if it feels too eager/sluggish.
15611574
#define DOUBLE_TAP_WINDOW_MS 300
15621575

@@ -1904,31 +1917,47 @@ void menu_layer_touch_handle_tap(MenuLayer *menu_layer, GPoint point_on_screen)
19041917
return;
19051918
}
19061919

1907-
// Priority 3 — select the tapped row through the full will_change contract (veto/redirect honoured)
1908-
// and centre it, WITHOUT activating. A veto (final == old selection) must change nothing: it must
1909-
// not re-centre the old selection (that mini-snap is exactly what Fix 1 removed from pans). Record
1910-
// the double-tap state only when a selection actually changes (normal or redirect).
1920+
// Priority 3 — the tapped row is not the selection: run the full will_change contract
1921+
// (veto/redirect honoured). A veto (final == old selection) must change nothing: no select, no
1922+
// activate, no re-centre of the old selection (that mini-snap is exactly what Fix 1 removed from
1923+
// pans), no animation cancel, and the double-tap window stays as it was — cancelling would abort
1924+
// an in-flight highlight animation and violate "veto changes nothing", so the cancel lives past
1925+
// the veto check.
19111926
const MenuIndex old_index = menu_layer->selection.index;
19121927
const MenuIndex final = prv_menu_run_will_change(menu_layer, candidate);
19131928
if (menu_index_compare(&final, &old_index) == 0) {
1914-
// Vetoed: no select, no activate, no re-centre, no animation cancel, and the window stays as it
1915-
// was. Cancelling here would abort an in-flight highlight animation and violate "veto changes
1916-
// nothing", so the cancel lives in the select branch below.
19171929
return;
19181930
}
19191931
prv_cancel_selection_animation(menu_layer);
1920-
// Animated: a center-focused menu plays the same jump/bounce as a button step (the nudge-in,
1921-
// half-way snap and bounce-out of prv_schedule_center_focus_animation); plain menus slide as
1922-
// before.
1932+
1933+
if (!menu_layer->center_focused) {
1934+
// Plain menus behave like a phone list: the tap selects AND opens the row in one gesture.
1935+
// Commit without scrolling — the row is already visible under the finger, and a re-centre
1936+
// would visibly shift the content just as the activated window pushes. Activate only when the
1937+
// contract kept the tapped row: a redirect selects the redirected row without opening it
1938+
// (opening a row the finger never touched would be a misfire), so there activation stays a
1939+
// deliberate second tap (priority 2). tap_select_only menus (short-item action menus, whose
1940+
// rows hold several columns the row-granular hit-test cannot tell apart) keep that two-step
1941+
// model for every row.
1942+
menu_layer_set_selected_index(menu_layer, final, MenuRowAlignNone, false);
1943+
if (!menu_layer->tap_select_only &&
1944+
menu_index_compare(&menu_layer->selection.index, &candidate) == 0) {
1945+
prv_menu_activate_selected(menu_layer);
1946+
}
1947+
return;
1948+
}
1949+
1950+
// Carousel: select the tapped row and centre it WITHOUT activating — the same jump/bounce as a
1951+
// button step (the nudge-in, half-way snap and bounce-out of
1952+
// prv_schedule_center_focus_animation); opening is the second tap (or the double-tap window).
19231953
menu_layer_set_selected_index(menu_layer, final, MenuRowAlignCenter, true);
19241954
// Record the committed (range-clamped) selection, not the raw will_change output: a client that
19251955
// redirects to an out-of-range index would otherwise be handed that OOB index by a fast double tap
1926-
// (priority 1), diverging from priority 2 which activates the clamped selection.index. On a
1927-
// center-focused menu the index commit is deferred to the half-way point of the jump animation;
1928-
// until then the committed target lives in animation.new_selection (a priority-1 tap during the
1929-
// jump cancels it, which drives the animation to its end state and commits that same target).
1930-
const bool jump_in_flight = menu_layer->center_focused &&
1931-
animation_is_scheduled(menu_layer->animation.animation);
1956+
// (priority 1), diverging from priority 2 which activates the clamped selection.index. The index
1957+
// commit is deferred to the half-way point of the jump animation; until then the committed
1958+
// target lives in animation.new_selection (a priority-1 tap during the jump cancels it, which
1959+
// drives the animation to its end state and commits that same target).
1960+
const bool jump_in_flight = animation_is_scheduled(menu_layer->animation.animation);
19321961
menu_layer->last_selected_index = jump_in_flight ? menu_layer->animation.new_selection.index
19331962
: menu_layer->selection.index;
19341963
prv_menu_set_last_select_ticks(menu_layer, now);

src/fw/applib/ui/menu_layer.h

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -413,10 +413,11 @@ typedef struct MenuLayer {
413413
} touch_nav_node;
414414

415415
//! @internal
416-
//! Tier-1 touch tap state for the "tap selects, second/double tap activates" behaviour: the last
417-
//! row a tap-select committed and when (\ref rtc_get_ticks). A second tap within the double-tap
418-
//! window activates that row without re-hit-testing (the row may have animated to the centre by
419-
//! then). Declared unconditionally (not under \c CONFIG_TOUCH) so \c sizeof(MenuLayer) is identical
416+
//! Tier-1 touch tap state for the center-focused "tap selects, second/double tap activates"
417+
//! behaviour: the last row a tap-select committed and when (\ref rtc_get_ticks). A second tap
418+
//! within the double-tap window activates that row without re-hit-testing (the row may have
419+
//! animated to the centre by then). Plain menus activate on the first tap and never arm this
420+
//! window. Declared unconditionally (not under \c CONFIG_TOUCH) so \c sizeof(MenuLayer) is identical
420421
//! on every board, and carved from \ref padding below like \ref touch_nav_node. The timestamp is
421422
//! split into two 32-bit halves rather than a single 8-byte \ref RtcTicks on purpose: a naturally
422423
//! 8-byte-aligned member would raise the struct's alignment to 8 and its size past the fixed
@@ -474,6 +475,14 @@ typedef struct MenuLayer {
474475
//! byte.
475476
bool touch_tap_swallow:1;
476477

478+
//! @internal
479+
//! True when a tap on a not-selected row of a plain (non-center-focused) menu must only select
480+
//! it, keeping activation a deliberate second tap, instead of the default select-and-activate.
481+
//! For menus whose rows hold several items (the action menu's short-item columns), where the
482+
//! row-granular tap hit-test cannot tell which item the finger meant. See
483+
//! \ref menu_layer_set_tap_select_only. Packs into \ref touch_fling_active's byte.
484+
bool tap_select_only:1;
485+
477486
//! Add some padding to keep track of the \ref MenuLayer size budget.
478487
//! As long as the size stays within this budget, 2.x apps can safely use the 3.x MenuLayer type.
479488
//! The actual size check is generated from applib_malloc.json, not asserted here.
@@ -685,6 +694,15 @@ bool menu_layer_get_center_focused(MenuLayer *menu_layer);
685694
//! @see \ref menu_layer_get_center_focused
686695
void menu_layer_set_center_focused(MenuLayer *menu_layer, bool center_focused);
687696

697+
//! @internal
698+
//! On a plain (non-center-focused) menu a touch tap on a not-selected row selects it and activates
699+
//! it in one gesture. Setting this makes such a tap only select, keeping activation a deliberate
700+
//! second tap — for menus whose rows hold several items (the action menu's short-item columns),
701+
//! where the row-granular tap hit-test cannot tell which item the finger meant.
702+
//! @param menu_layer The menu layer for which to enable or disable the behavior.
703+
//! @param tap_select_only true = a tap only selects, false (default) = a tap selects and activates.
704+
void menu_layer_set_tap_select_only(MenuLayer *menu_layer, bool tap_select_only);
705+
688706
//! True, if the \ref MenuLayer can wrap around the first and last element.
689707
//! @see \ref menu_layer_set_scroll_wrap_around
690708
bool menu_layer_get_scroll_wrap_around(MenuLayer *menu_layer);

tests/fw/ui/test_action_menu_layer.c

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,18 +236,15 @@ void test_action_menu_layer__tap_on_selected_item_activates(void) {
236236
action_menu_layer_deinit(&s_aml);
237237
}
238238

239-
// A tap on a different action selects it (no activation); a later tap on it activates it.
240-
void test_action_menu_layer__tap_other_item_selects_then_activates(void) {
239+
// A wide-item row holds exactly one action, so a tap on a different action selects it and
240+
// activates it in the same gesture (plain menus open on a single tap).
241+
void test_action_menu_layer__tap_other_item_selects_and_activates(void) {
241242
prv_init_aml_with_wide_items();
242243

243244
menu_layer_touch_handle_tap(&s_aml.menu_layer, prv_tap_point_for_row(&s_aml.menu_layer, 2));
244-
cl_assert_equal_i(s_select_count, 0);
245245
cl_assert_equal_i(s_aml.selected_index, 2);
246246
cl_assert_equal_i(s_selection_changed_count, 1);
247247
cl_assert(s_last_changed_item == &s_wide_items[2]);
248-
249-
prv_advance_past_double_tap_window();
250-
menu_layer_touch_handle_tap(&s_aml.menu_layer, prv_tap_point_for_row(&s_aml.menu_layer, 2));
251248
cl_assert_equal_i(s_select_count, 1);
252249
cl_assert(s_last_selected_item == &s_wide_items[2]);
253250

0 commit comments

Comments
 (0)