Skip to content

Commit 8408daf

Browse files
committed
fw/applib/ui/menu_layer: add scroll wrap around and vibe behavior capabilities
Signed-off-by: Paul Chanvin <[email protected]>
1 parent fc3167d commit 8408daf

File tree

2 files changed

+191
-1
lines changed

2 files changed

+191
-1
lines changed

src/fw/applib/ui/menu_layer.c

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
#include "system/logging.h"
2222
#include "system/passert.h"
2323
#include "util/math.h"
24+
#include "util/size.h"
25+
#include "vibes.h"
2426

2527
#include <string.h>
2628

@@ -76,16 +78,103 @@ static void prv_menu_select_long_click_handler(ClickRecognizerRef recognizer,
7678
}
7779
}
7880

81+
static inline uint16_t prv_menu_layer_get_num_sections(MenuLayer *menu_layer);
82+
static inline uint16_t prv_menu_layer_get_num_rows(MenuLayer *menu_layer, uint16_t section_index);
83+
84+
static bool prv_menu_index_is_first_index(MenuLayer *menu_layer, const MenuIndex *index) {
85+
(void)menu_layer;
86+
87+
MenuIndex first_index = MenuIndex(0, 0);
88+
return menu_index_compare(index, &first_index) == 0;
89+
}
90+
91+
static bool prv_menu_index_is_last_index(MenuLayer *menu_layer, const MenuIndex *index) {
92+
int last_index_section = prv_menu_layer_get_num_sections(menu_layer) - 1;
93+
int last_index_row = prv_menu_layer_get_num_rows(menu_layer, last_index_section) - 1;
94+
MenuIndex last_index = MenuIndex(last_index_section, last_index_row);
95+
return menu_index_compare(index, &last_index) == 0;
96+
}
97+
98+
static void prv_vibe_pulse(void) {
99+
uint32_t const segments[] = { 50 };
100+
VibePattern pat = {
101+
.durations = segments,
102+
.num_segments = ARRAY_LENGTH(segments),
103+
};
104+
vibes_enqueue_custom_pattern(pat);
105+
}
106+
107+
//! Handle the menu scroll wrap around
108+
//! @param menu_layer reference to the current MenuLayer
109+
//! @param recognizer reference to the ClickRecognizer struct
110+
//! @param scrolling_up `true` if scrolling up, `false` if scrolling down
111+
//! @return `true` if a wrap around has been applied
112+
static bool prv_menu_scroll_handle_wrap_around(MenuLayer *menu_layer, ClickRecognizerRef recognizer, bool scrolling_up) {
113+
const uint8_t current_scroll_action = scrolling_up ? MenuLayerRepeatScrollingUp : MenuLayerRepeatScrollingDown;
114+
const bool is_repeating = click_recognizer_is_repeating(recognizer);
115+
116+
if (is_repeating) {
117+
menu_layer->cache.button_repeat_scrolling = current_scroll_action;
118+
if (!menu_layer->scroll_force_wrap_on_repeat) {
119+
return false;
120+
}
121+
}
122+
menu_layer->cache.button_repeat_scrolling = MenuLayerNoRepeatScrolling;
123+
124+
MenuIndex current_index = menu_layer->selection.index;
125+
int last_index_section = prv_menu_layer_get_num_sections(menu_layer) - 1;
126+
int last_index_row = prv_menu_layer_get_num_rows(menu_layer, last_index_section) - 1;
127+
MenuIndex first_index = MenuIndex(0, 0);
128+
MenuIndex last_index = MenuIndex(last_index_section, last_index_row);
129+
MenuIndex *wraparound_dest_index;
130+
if ((menu_index_compare(&current_index, &first_index) == 0) && scrolling_up) {
131+
wraparound_dest_index = &last_index;
132+
} else if ((menu_index_compare(&current_index, &last_index) == 0) && !scrolling_up) {
133+
wraparound_dest_index = &first_index;
134+
} else {
135+
return false;
136+
}
137+
138+
const bool animated = true;
139+
menu_layer_set_selected_index(menu_layer, *wraparound_dest_index, MenuRowAlignCenter, animated);
140+
if (menu_layer->scroll_vibe_on_wrap_around) {
141+
prv_vibe_pulse();
142+
}
143+
return true;
144+
}
145+
79146
void menu_up_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
80147
const bool up = true;
148+
if (menu_layer->scroll_wrap_around && prv_menu_scroll_handle_wrap_around(menu_layer, recognizer, up)) {
149+
return;
150+
}
151+
152+
MenuIndex prev_index = menu_layer->selection.index;
81153
const bool animated = true;
82154
menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
155+
MenuIndex current_index = menu_layer->selection.index;
156+
if ((menu_layer->scroll_vibe_on_blocked) &&
157+
(menu_index_compare(&current_index, &prev_index) == 0) &&
158+
(prv_menu_index_is_first_index(menu_layer, &current_index))) {
159+
prv_vibe_pulse();
160+
}
83161
}
84162

85163
void menu_down_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
86164
const bool up = false;
165+
if (menu_layer->scroll_wrap_around && prv_menu_scroll_handle_wrap_around(menu_layer, recognizer, up)) {
166+
return;
167+
}
168+
169+
MenuIndex prev_index = menu_layer->selection.index;
87170
const bool animated = true;
88171
menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
172+
MenuIndex current_index = menu_layer->selection.index;
173+
if ((menu_layer->scroll_vibe_on_blocked) &&
174+
(menu_index_compare(&current_index, &prev_index) == 0) &&
175+
(prv_menu_index_is_last_index(menu_layer, &current_index))) {
176+
prv_vibe_pulse();
177+
}
89178
}
90179

91180
static void prv_menu_click_config_provider(MenuLayer *menu_layer) {
@@ -1293,3 +1382,46 @@ void menu_layer_set_center_focused(MenuLayer *menu_layer, bool center_focused) {
12931382
prv_set_center_focused(menu_layer, center_focused);
12941383
menu_layer_update_caches(menu_layer);
12951384
}
1385+
1386+
bool menu_layer_get_scroll_wrap_around(MenuLayer *menu_layer) {
1387+
return menu_layer->scroll_wrap_around;
1388+
}
1389+
1390+
void menu_layer_set_scroll_wrap_around(MenuLayer *menu_layer, bool scroll_wrap_around) {
1391+
if (!menu_layer) {
1392+
return;
1393+
}
1394+
menu_layer->scroll_wrap_around = scroll_wrap_around;
1395+
}
1396+
1397+
uint8_t menu_layer_get_scroll_vibe_behavior(MenuLayer *menu_layer) {
1398+
if (menu_layer->scroll_vibe_on_blocked) {
1399+
return 2;
1400+
} else if (menu_layer->scroll_vibe_on_wrap_around) {
1401+
return 1;
1402+
} else {
1403+
return 0;
1404+
}
1405+
}
1406+
1407+
void menu_layer_set_scroll_vibe_on_wrap(MenuLayer *menu_layer, bool scroll_vibe_on_wrap) {
1408+
if (!menu_layer) {
1409+
return;
1410+
}
1411+
1412+
if (scroll_vibe_on_wrap) {
1413+
menu_layer->scroll_vibe_on_blocked = false;
1414+
}
1415+
menu_layer->scroll_vibe_on_wrap_around = scroll_vibe_on_wrap;
1416+
}
1417+
1418+
void menu_layer_set_scroll_vibe_on_blocked(MenuLayer *menu_layer, bool scroll_vibe_on_blocked) {
1419+
if (!menu_layer) {
1420+
return;
1421+
}
1422+
1423+
if (scroll_vibe_on_blocked) {
1424+
menu_layer->scroll_vibe_on_wrap_around = false;
1425+
}
1426+
menu_layer->scroll_vibe_on_blocked = scroll_vibe_on_blocked;
1427+
}

src/fw/applib/ui/menu_layer.h

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,12 @@ enum {
338338
_Static_assert(MenuLayerColor_Count == 2, "Bad enum MenuLayerColor");
339339
#endif
340340

341+
enum {
342+
MenuLayerNoRepeatScrolling = 0,
343+
MenuLayerRepeatScrollingUp = 1,
344+
MenuLayerRepeatScrollingDown = 2,
345+
};
346+
341347
//! Data structure of a MenuLayer.
342348
//! @note a `MenuLayer *` can safely be casted to a `Layer *` and
343349
//! `ScrollLayer *` and can thus be used with all other functions that take a
@@ -359,6 +365,8 @@ typedef struct MenuLayer {
359365
//! @internal
360366
//! Cell index + geometry cache of a cell that was in frame during the last redraw
361367
MenuCellSpan cursor;
368+
369+
uint8_t button_repeat_scrolling:2;
362370
} cache;
363371
//! @internal
364372
//! Selected cell index + geometery cache of the selected cell
@@ -401,10 +409,24 @@ typedef struct MenuLayer {
401409
//! independent of the scrolling animation.
402410
bool selection_animation_disabled:1;
403411

412+
//! If true, the MenuLayer will be able to wrap around the first element and the last element
413+
//! when scrolling.
414+
bool scroll_wrap_around:1;
415+
416+
//! If this is true alongside \ref scroll_wrap_around, the MenuLayer will be able to wrap around
417+
//! even when the 'up' or 'down' button is held down.
418+
bool scroll_force_wrap_on_repeat:1;
419+
420+
//! If True, a vibration will occur when wrapping around.
421+
bool scroll_vibe_on_wrap_around:1;
422+
423+
//! If True, a vibration will occur when cursor is getting blocked at the top or bottom
424+
bool scroll_vibe_on_blocked:1;
425+
404426
//! Add some padding to keep track of the \ref MenuLayer size budget.
405427
//! As long as the size stays within this budget, 2.x apps can safely use the 3.x MenuLayer type.
406428
//! When padding is removed, the assertion below should also be removed.
407-
uint8_t padding[44];
429+
uint8_t padding[40];
408430
} MenuLayer;
409431

410432
//! Padding used below the last item in pixels
@@ -612,6 +634,42 @@ bool menu_layer_get_center_focused(MenuLayer *menu_layer);
612634
//! @see \ref menu_layer_get_center_focused
613635
void menu_layer_set_center_focused(MenuLayer *menu_layer, bool center_focused);
614636

637+
//! True, if the \ref MenuLayer can wrap around the first and last element.
638+
//! @see \ref menu_layer_set_scroll_wrap_around
639+
bool menu_layer_get_scroll_wrap_around(MenuLayer *menu_layer);
640+
641+
//! Controls if the \ref MenuLayer can wrap around from the first element to the last when going
642+
//! up and from the last element to the first when going down.
643+
//! Even enabled, wrap around will stay disabled when holding down the navigation buttons (up or down).
644+
//! Defaults to false for every platform
645+
//! @param menu_layer The menu layer for which to enable or disable the behavior.
646+
//! @param scroll_wrap_around true = enable the wrap around, false = disable it.
647+
//! @see \ref menu_layer_get_scroll_wrap_around
648+
void menu_layer_set_scroll_wrap_around(MenuLayer *menu_layer, bool scroll_wrap_around);
649+
650+
//! Return a number depending on scroll vibe behavior :
651+
//! - 0 : no vibe will occur
652+
//! - 1 : vibe will occur on wrap around
653+
//! - 2 : vibe will occur when stuck at the top or bottom of the menu
654+
//! @see \ref menu_layer_set_scroll_vibe_on_wrap
655+
//! @see \ref menu_layer_set_scroll_vibe_on_blocked
656+
uint8_t menu_layer_get_scroll_vibe_behavior(MenuLayer *menu_layer);
657+
658+
//! Controls if the \ref MenuLayer wil generate a vibe when wrapping around.
659+
//! Defaults to false for every platform
660+
//! @param menu_layer The menu layer for which to enable or disable the behavior.
661+
//! @param scroll_vibe_on_wrap true = enable the vibe on wrap around, false = disable it.
662+
//! @see \ref menu_layer_get_scroll_vibe_behavior
663+
//! @see \ref menu_layer_set_scroll_vibe_on_blocked
664+
void menu_layer_set_scroll_vibe_on_wrap(MenuLayer *menu_layer, bool scroll_vibe_on_wrap);
665+
666+
//! Controls if the \ref MenuLayer wil generate a vibe when blocked at the top or bottom.
667+
//! Defaults to false for every platform
668+
//! @param menu_layer The menu layer for which to enable or disable the behavior.
669+
//! @param scroll_vibe_on_wrap true = enable the vibe on cursor block, false = disable it.
670+
//! @see \ref menu_layer_get_scroll_vibe_behavior
671+
//! @see \ref menu_layer_set_scroll_vibe_on_wrap
672+
void menu_layer_set_scroll_vibe_on_blocked(MenuLayer *menu_layer, bool scroll_vibe_on_blocked);
615673

616674
//! @} // end addtogroup MenuLayer
617675
//! @} // end addtogroup Layer

0 commit comments

Comments
 (0)