1 /**
2  * @file lv_gridnav.c
3  *
4  */
5 
6 /*********************
7  *      INCLUDES
8  *********************/
9 #include "lv_gridnav.h"
10 #if LV_USE_GRIDNAV
11 
12 #include "../../../misc/lv_assert.h"
13 #include "../../../misc/lv_math.h"
14 #include "../../../core/lv_indev.h"
15 
16 /*********************
17  *      DEFINES
18  *********************/
19 
20 /**********************
21  *      TYPEDEFS
22  **********************/
23 typedef struct {
24     lv_gridnav_ctrl_t ctrl;
25     lv_obj_t * focused_obj;
26 } lv_gridnav_dsc_t;
27 
28 typedef enum {
29     FIND_LEFT,
30     FIND_RIGHT,
31     FIND_TOP,
32     FIND_BOTTOM,
33     FIND_NEXT_ROW_FIRST_ITEM,
34     FIND_PREV_ROW_LAST_ITEM,
35     FIND_FIRST_ROW,
36     FIND_LAST_ROW,
37 } find_mode_t;
38 
39 /**********************
40  *  STATIC PROTOTYPES
41  **********************/
42 static void gridnav_event_cb(lv_event_t * e);
43 static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode);
44 static lv_obj_t * find_first_focusable(lv_obj_t * obj);
45 static lv_obj_t * find_last_focusable(lv_obj_t * obj);
46 static bool obj_is_focuable(lv_obj_t * obj);
47 static lv_coord_t get_x_center(lv_obj_t * obj);
48 static lv_coord_t get_y_center(lv_obj_t * obj);
49 
50 /**********************
51  *  STATIC VARIABLES
52  **********************/
53 
54 /**********************
55  *      MACROS
56  **********************/
57 
58 /**********************
59  *   GLOBAL FUNCTIONS
60  **********************/
61 
lv_gridnav_add(lv_obj_t * obj,lv_gridnav_ctrl_t ctrl)62 void lv_gridnav_add(lv_obj_t * obj, lv_gridnav_ctrl_t ctrl)
63 {
64     lv_gridnav_remove(obj); /*Be sure to not add gridnav twice*/
65 
66     lv_gridnav_dsc_t * dsc = lv_mem_alloc(sizeof(lv_gridnav_dsc_t));
67     LV_ASSERT_MALLOC(dsc);
68     dsc->ctrl = ctrl;
69     dsc->focused_obj = NULL;
70     lv_obj_add_event_cb(obj, gridnav_event_cb, LV_EVENT_ALL, dsc);
71 
72     lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLL_WITH_ARROW);
73 }
74 
lv_gridnav_remove(lv_obj_t * obj)75 void lv_gridnav_remove(lv_obj_t * obj)
76 {
77     lv_gridnav_dsc_t * dsc = lv_obj_get_event_user_data(obj, gridnav_event_cb);
78     if(dsc == NULL) return; /* no gridnav on this object */
79 
80     lv_mem_free(dsc);
81     lv_obj_remove_event_cb(obj, gridnav_event_cb);
82 }
83 
84 /**********************
85  *   STATIC FUNCTIONS
86  **********************/
87 
gridnav_event_cb(lv_event_t * e)88 static void gridnav_event_cb(lv_event_t * e)
89 {
90     lv_obj_t * obj = lv_event_get_current_target(e);
91     lv_gridnav_dsc_t * dsc = lv_event_get_user_data(e);
92     lv_event_code_t code = lv_event_get_code(e);
93 
94     if(code == LV_EVENT_KEY) {
95         uint32_t child_cnt = lv_obj_get_child_cnt(obj);
96         if(child_cnt == 0) return;
97 
98         if(dsc->focused_obj == NULL) dsc->focused_obj = find_first_focusable(obj);
99         if(dsc->focused_obj == NULL) return;
100 
101         uint32_t key = lv_indev_get_key(lv_indev_get_act());
102         lv_obj_t * guess = NULL;
103 
104         if(key == LV_KEY_RIGHT) {
105             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
106                lv_obj_get_scroll_right(dsc->focused_obj) > 0) {
107                 lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4;
108                 if(d <= 0) d = 1;
109                 lv_obj_scroll_by_bounded(dsc->focused_obj, -d, 0, LV_ANIM_ON);
110             }
111             else {
112                 guess = find_chid(obj, dsc->focused_obj, FIND_RIGHT);
113                 if(guess == NULL) {
114                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
115                         guess = find_chid(obj, dsc->focused_obj, FIND_NEXT_ROW_FIRST_ITEM);
116                         if(guess == NULL) guess = find_first_focusable(obj);
117                     }
118                     else {
119                         lv_group_focus_next(lv_obj_get_group(obj));
120                     }
121                 }
122             }
123         }
124         else if(key == LV_KEY_LEFT) {
125             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
126                lv_obj_get_scroll_left(dsc->focused_obj) > 0) {
127                 lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4;
128                 if(d <= 0) d = 1;
129                 lv_obj_scroll_by_bounded(dsc->focused_obj, d, 0, LV_ANIM_ON);
130             }
131             else {
132                 guess = find_chid(obj, dsc->focused_obj, FIND_LEFT);
133                 if(guess == NULL) {
134                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
135                         guess = find_chid(obj, dsc->focused_obj, FIND_PREV_ROW_LAST_ITEM);
136                         if(guess == NULL) guess = find_last_focusable(obj);
137                     }
138                     else {
139                         lv_group_focus_prev(lv_obj_get_group(obj));
140                     }
141                 }
142             }
143         }
144         else if(key == LV_KEY_DOWN) {
145             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
146                lv_obj_get_scroll_bottom(dsc->focused_obj) > 0) {
147                 lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4;
148                 if(d <= 0) d = 1;
149                 lv_obj_scroll_by_bounded(dsc->focused_obj, 0, -d, LV_ANIM_ON);
150             }
151             else {
152                 guess = find_chid(obj, dsc->focused_obj, FIND_BOTTOM);
153                 if(guess == NULL) {
154                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
155                         guess = find_chid(obj, dsc->focused_obj, FIND_FIRST_ROW);
156                     }
157                     else {
158                         lv_group_focus_next(lv_obj_get_group(obj));
159                     }
160                 }
161             }
162         }
163         else if(key == LV_KEY_UP) {
164             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
165                lv_obj_get_scroll_top(dsc->focused_obj) > 0) {
166                 lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4;
167                 if(d <= 0) d = 1;
168                 lv_obj_scroll_by_bounded(dsc->focused_obj, 0, d, LV_ANIM_ON);
169             }
170             else {
171                 guess = find_chid(obj, dsc->focused_obj, FIND_TOP);
172                 if(guess == NULL) {
173                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
174                         guess = find_chid(obj, dsc->focused_obj, FIND_LAST_ROW);
175                     }
176                     else {
177                         lv_group_focus_prev(lv_obj_get_group(obj));
178                     }
179                 }
180             }
181         }
182         else {
183             if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
184                 lv_event_send(dsc->focused_obj, LV_EVENT_KEY, &key);
185             }
186         }
187 
188         if(guess && guess != dsc->focused_obj) {
189             lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
190             lv_obj_add_state(guess, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
191             lv_obj_scroll_to_view(guess, LV_ANIM_ON);
192             dsc->focused_obj = guess;
193         }
194     }
195     else if(code == LV_EVENT_FOCUSED) {
196         if(dsc->focused_obj == NULL)  dsc->focused_obj = find_first_focusable(obj);
197         if(dsc->focused_obj) {
198             lv_obj_add_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
199             lv_obj_scroll_to_view(dsc->focused_obj, LV_ANIM_OFF);
200         }
201     }
202     else if(code == LV_EVENT_DEFOCUSED) {
203         if(dsc->focused_obj) {
204             lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
205         }
206     }
207     else if(code == LV_EVENT_CHILD_CREATED) {
208         lv_obj_t * child = lv_event_get_target(e);
209         if(lv_obj_get_parent(child) == obj) {
210             if(dsc->focused_obj == NULL) {
211                 dsc->focused_obj = child;
212                 if(lv_obj_has_state(obj, LV_STATE_FOCUSED)) {
213                     lv_obj_add_state(child, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
214                     lv_obj_scroll_to_view(child, LV_ANIM_OFF);
215                 }
216             }
217         }
218     }
219     else if(code == LV_EVENT_CHILD_DELETED) {
220         /*This event bubble, so be sure this object's child was deleted.
221          *As we don't know which object was deleted we can't make the next focused.
222          *So make the first object focused*/
223         lv_obj_t * target = lv_event_get_target(e);
224         if(target == obj) {
225             dsc->focused_obj = find_first_focusable(obj);
226         }
227     }
228     else if(code == LV_EVENT_DELETE) {
229         lv_gridnav_remove(obj);
230     }
231     else if(code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING || code == LV_EVENT_PRESS_LOST ||
232             code == LV_EVENT_LONG_PRESSED || code == LV_EVENT_LONG_PRESSED_REPEAT ||
233             code == LV_EVENT_CLICKED || code == LV_EVENT_RELEASED) {
234         if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
235             /*Forward press/release related event too*/
236             lv_indev_type_t t = lv_indev_get_type(lv_indev_get_act());
237             if(t == LV_INDEV_TYPE_ENCODER || t == LV_INDEV_TYPE_KEYPAD) {
238                 lv_event_send(dsc->focused_obj, code, lv_indev_get_act());
239             }
240         }
241     }
242 }
243 
find_chid(lv_obj_t * obj,lv_obj_t * start_child,find_mode_t mode)244 static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode)
245 {
246     lv_coord_t x_start = get_x_center(start_child);
247     lv_coord_t y_start = get_y_center(start_child);
248     uint32_t child_cnt = lv_obj_get_child_cnt(obj);
249     lv_obj_t * guess = NULL;
250     lv_coord_t x_err_guess = LV_COORD_MAX;
251     lv_coord_t y_err_guess = LV_COORD_MAX;
252     lv_coord_t h_half = lv_obj_get_height(start_child) / 2;
253     lv_coord_t h_max = lv_obj_get_height(obj) + lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj);
254     uint32_t i;
255     for(i = 0; i < child_cnt; i++) {
256         lv_obj_t * child = lv_obj_get_child(obj, i);
257         if(child == start_child) continue;
258         if(obj_is_focuable(child) == false) continue;
259 
260         lv_coord_t x_err = 0;
261         lv_coord_t y_err = 0;
262         switch(mode) {
263             case FIND_LEFT:
264                 x_err = get_x_center(child) - x_start;
265                 y_err = get_y_center(child) - y_start;
266                 if(x_err >= 0) continue;    /*It's on the right*/
267                 if(LV_ABS(y_err) > h_half) continue;    /*Too far*/
268                 break;
269             case FIND_RIGHT:
270                 x_err = get_x_center(child) - x_start;
271                 y_err = get_y_center(child) - y_start;
272                 if(x_err <= 0) continue;    /*It's on the left*/
273                 if(LV_ABS(y_err) > h_half) continue;    /*Too far*/
274                 break;
275             case FIND_TOP:
276                 x_err = get_x_center(child) - x_start;
277                 y_err = get_y_center(child) - y_start;
278                 if(y_err >= 0) continue;    /*It's on the bottom*/
279                 break;
280             case FIND_BOTTOM:
281                 x_err = get_x_center(child) - x_start;
282                 y_err = get_y_center(child) - y_start;
283                 if(y_err <= 0) continue;    /*It's on the top*/
284                 break;
285             case FIND_NEXT_ROW_FIRST_ITEM:
286                 y_err = get_y_center(child) - y_start;
287                 if(y_err <= 0) continue;    /*It's on the top*/
288                 x_err = lv_obj_get_x(child);
289                 break;
290             case FIND_PREV_ROW_LAST_ITEM:
291                 y_err = get_y_center(child) - y_start;
292                 if(y_err >= 0) continue;    /*It's on the bottom*/
293                 x_err = obj->coords.x2 - child->coords.x2;
294                 break;
295             case FIND_FIRST_ROW:
296                 x_err = get_x_center(child) - x_start;
297                 y_err = lv_obj_get_y(child);
298                 break;
299             case FIND_LAST_ROW:
300                 x_err = get_x_center(child) - x_start;
301                 y_err = h_max - lv_obj_get_y(child);
302         }
303 
304         if(guess == NULL ||
305            (y_err * y_err + x_err * x_err < y_err_guess * y_err_guess + x_err_guess * x_err_guess)) {
306             guess = child;
307             x_err_guess  = x_err;
308             y_err_guess  = y_err;
309         }
310     }
311     return guess;
312 }
313 
find_first_focusable(lv_obj_t * obj)314 static lv_obj_t * find_first_focusable(lv_obj_t * obj)
315 {
316     uint32_t child_cnt = lv_obj_get_child_cnt(obj);
317     uint32_t i;
318     for(i = 0; i < child_cnt; i++) {
319         lv_obj_t * child = lv_obj_get_child(obj, i);
320         if(obj_is_focuable(child)) return child;
321 
322     }
323     return NULL;
324 }
325 
find_last_focusable(lv_obj_t * obj)326 static lv_obj_t * find_last_focusable(lv_obj_t * obj)
327 {
328     uint32_t child_cnt = lv_obj_get_child_cnt(obj);
329     int32_t i;
330     for(i = child_cnt - 1; i >= 0; i++) {
331         lv_obj_t * child = lv_obj_get_child(obj, i);
332         if(obj_is_focuable(child)) return child;
333     }
334     return NULL;
335 }
336 
obj_is_focuable(lv_obj_t * obj)337 static bool obj_is_focuable(lv_obj_t * obj)
338 {
339     if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return false;
340     if(lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE)) return true;
341     else return false;
342 }
343 
get_x_center(lv_obj_t * obj)344 static lv_coord_t get_x_center(lv_obj_t * obj)
345 {
346     return obj->coords.x1 + lv_area_get_width(&obj->coords) / 2;
347 }
348 
get_y_center(lv_obj_t * obj)349 static lv_coord_t get_y_center(lv_obj_t * obj)
350 {
351     return obj->coords.y1 + lv_area_get_height(&obj->coords) / 2;
352 }
353 
354 #endif /*LV_USE_GRIDNAV*/
355