Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions src/fw/applib/ui/menu_layer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1062,10 +1062,16 @@ static void prv_menu_layer_update_selection_highlight(MenuLayer *menu_layer, boo
}

static MenuRowAlign prv_corrected_scroll_align(MenuLayer *menu_layer, MenuRowAlign align) {
if (menu_layer->center_focused) {
return MenuRowAlignCenter;
if (!menu_layer->center_focused) {
return align;
}
return align;
#ifdef CONFIG_TOUCH
// While a touch gesture owns this menu, the finger owns the scroll offset (the carousel settles to the centre on liftoff via prv_menu_touch_settle_to_center, never through here). MenuRowAlignNone must keep its "leave the offset where it is" meaning in that window: a reload's update_caches re-selects the current row with None, and promoting that to a re-centre yanks the offset under the finger, which the next pan update jumps straight back from the gesture's base offset.
if (menu_layer_touch_is_gesture_target(menu_layer)) {
return align;
}
#endif
return MenuRowAlignCenter;
}

static void prv_menu_layer_update_selection_scroll_position(MenuLayer *menu_layer,
Expand Down
34 changes: 34 additions & 0 deletions tests/fw/ui/test_menu_layer.c
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,40 @@ void test_menu_layer__dispatch_tap_redirect_selects_target(void) {

// ---- Pan through the dispatcher scrolls 1:1 and snaps on liftoff, selection frozen ----

// A data reload landing mid-pan on a center-focused carousel must not touch the scroll offset: the finger owns it until liftoff. update_caches re-selects the current row with MenuRowAlignNone, which the center-focused promotion turns into a re-centre -- yanking the offset under the finger, which the next pan update immediately jumps back from the gesture's base offset (a visible double-jump, and a desync for the rest of the gesture).
void test_menu_layer__dispatch_pan_reload_mid_gesture_keeps_offset(void) {
prv_touch_nav_setup();
MenuLayer l;
menu_layer_init(&l, &GRect(0, 0, 200, 300));
menu_layer_set_center_focused(&l, true);
prv_set_touch_callbacks(&l);
menu_layer_reload_data(&l);
layer_add_child(&s_root_layer, menu_layer_get_layer(&l));
const int16_t base = scroll_layer_get_content_offset(&l.scroll_layer).y; // row 0 centred
prv_reset_touch_counters();

prv_drive(TouchEvent_Touchdown, 100, 150);
prv_advance_ms(20);
prv_drive(TouchEvent_PositionUpdate, 100, 110); // 40px up -> pan Started
prv_advance_ms(20);
prv_drive(TouchEvent_PositionUpdate, 100, 80); // Updated -> live scroll
cl_assert(menu_layer_touch_is_gesture_target(&l));
const int16_t mid_pan = scroll_layer_get_content_offset(&l.scroll_layer).y;
cl_assert(mid_pan != base); // the pan actually moved the content

// The reload (timer, inbox, timeline update) lands while the finger is still down.
menu_layer_reload_data(&l);
cl_assert_equal_i(scroll_layer_get_content_offset(&l.scroll_layer).y, mid_pan);

// Liftoff still settles the carousel: the glide is animated, so (as in the direct-handler settle test) assert it was scheduled rather than driven to its target.
prv_advance_ms(20);
prv_drive(TouchEvent_Liftoff, 100, 80);
Animation *settle = property_animation_get_animation(l.scroll_layer.animation);
cl_assert(settle != NULL);
cl_assert(animation_is_scheduled(settle));
menu_layer_deinit(&l);
}

void test_menu_layer__dispatch_pan_scrolls_1to1_and_snaps(void) {
prv_touch_nav_setup();
MenuLayer l;
Expand Down