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 
lv_gridnav_set_focused(lv_obj_t * cont,lv_obj_t * to_focus,lv_anim_enable_t anim_en)84 void lv_gridnav_set_focused(lv_obj_t * cont, lv_obj_t * to_focus, lv_anim_enable_t anim_en)
85 {
86     LV_ASSERT_NULL(to_focus);
87     lv_gridnav_dsc_t * dsc = lv_obj_get_event_user_data(cont, gridnav_event_cb);
88     if(dsc == NULL) {
89         LV_LOG_WARN("`cont` is not a gridnav container");
90         return;
91     }
92 
93     if(obj_is_focuable(to_focus) == false) {
94         LV_LOG_WARN("The object to focus is not focusable");
95         return;
96     }
97 
98     lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
99     lv_obj_add_state(to_focus, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
100     lv_obj_scroll_to_view(to_focus, anim_en);
101     dsc->focused_obj = to_focus;
102 
103 }
104 
105 /**********************
106  *   STATIC FUNCTIONS
107  **********************/
108 
gridnav_event_cb(lv_event_t * e)109 static void gridnav_event_cb(lv_event_t * e)
110 {
111     lv_obj_t * obj = lv_event_get_current_target(e);
112     lv_gridnav_dsc_t * dsc = lv_event_get_user_data(e);
113     lv_event_code_t code = lv_event_get_code(e);
114 
115     if(code == LV_EVENT_KEY) {
116         uint32_t child_cnt = lv_obj_get_child_cnt(obj);
117         if(child_cnt == 0) return;
118 
119         if(dsc->focused_obj == NULL) dsc->focused_obj = find_first_focusable(obj);
120         if(dsc->focused_obj == NULL) return;
121 
122         uint32_t key = lv_event_get_key(e);
123         lv_obj_t * guess = NULL;
124 
125         if(key == LV_KEY_RIGHT) {
126             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
127                lv_obj_get_scroll_right(dsc->focused_obj) > 0) {
128                 lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4;
129                 if(d <= 0) d = 1;
130                 lv_obj_scroll_by_bounded(dsc->focused_obj, -d, 0, LV_ANIM_ON);
131             }
132             else {
133                 guess = find_chid(obj, dsc->focused_obj, FIND_RIGHT);
134                 if(guess == NULL) {
135                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
136                         guess = find_chid(obj, dsc->focused_obj, FIND_NEXT_ROW_FIRST_ITEM);
137                         if(guess == NULL) guess = find_first_focusable(obj);
138                     }
139                     else {
140                         lv_group_focus_next(lv_obj_get_group(obj));
141                     }
142                 }
143             }
144         }
145         else if(key == LV_KEY_LEFT) {
146             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
147                lv_obj_get_scroll_left(dsc->focused_obj) > 0) {
148                 lv_coord_t d = lv_obj_get_width(dsc->focused_obj) / 4;
149                 if(d <= 0) d = 1;
150                 lv_obj_scroll_by_bounded(dsc->focused_obj, d, 0, LV_ANIM_ON);
151             }
152             else {
153                 guess = find_chid(obj, dsc->focused_obj, FIND_LEFT);
154                 if(guess == NULL) {
155                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
156                         guess = find_chid(obj, dsc->focused_obj, FIND_PREV_ROW_LAST_ITEM);
157                         if(guess == NULL) guess = find_last_focusable(obj);
158                     }
159                     else {
160                         lv_group_focus_prev(lv_obj_get_group(obj));
161                     }
162                 }
163             }
164         }
165         else if(key == LV_KEY_DOWN) {
166             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
167                lv_obj_get_scroll_bottom(dsc->focused_obj) > 0) {
168                 lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4;
169                 if(d <= 0) d = 1;
170                 lv_obj_scroll_by_bounded(dsc->focused_obj, 0, -d, LV_ANIM_ON);
171             }
172             else {
173                 guess = find_chid(obj, dsc->focused_obj, FIND_BOTTOM);
174                 if(guess == NULL) {
175                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
176                         guess = find_chid(obj, dsc->focused_obj, FIND_FIRST_ROW);
177                     }
178                     else {
179                         lv_group_focus_next(lv_obj_get_group(obj));
180                     }
181                 }
182             }
183         }
184         else if(key == LV_KEY_UP) {
185             if((dsc->ctrl & LV_GRIDNAV_CTRL_SCROLL_FIRST) && lv_obj_has_flag(dsc->focused_obj, LV_OBJ_FLAG_SCROLLABLE) &&
186                lv_obj_get_scroll_top(dsc->focused_obj) > 0) {
187                 lv_coord_t d = lv_obj_get_height(dsc->focused_obj) / 4;
188                 if(d <= 0) d = 1;
189                 lv_obj_scroll_by_bounded(dsc->focused_obj, 0, d, LV_ANIM_ON);
190             }
191             else {
192                 guess = find_chid(obj, dsc->focused_obj, FIND_TOP);
193                 if(guess == NULL) {
194                     if(dsc->ctrl & LV_GRIDNAV_CTRL_ROLLOVER) {
195                         guess = find_chid(obj, dsc->focused_obj, FIND_LAST_ROW);
196                     }
197                     else {
198                         lv_group_focus_prev(lv_obj_get_group(obj));
199                     }
200                 }
201             }
202         }
203         else {
204             if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
205                 lv_event_send(dsc->focused_obj, LV_EVENT_KEY, &key);
206             }
207         }
208 
209         if(guess && guess != dsc->focused_obj) {
210             lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
211             lv_obj_add_state(guess, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
212             lv_obj_scroll_to_view(guess, LV_ANIM_ON);
213             dsc->focused_obj = guess;
214         }
215     }
216     else if(code == LV_EVENT_FOCUSED) {
217         if(dsc->focused_obj == NULL)  dsc->focused_obj = find_first_focusable(obj);
218         if(dsc->focused_obj) {
219             lv_obj_add_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
220             lv_obj_clear_state(dsc->focused_obj, LV_STATE_PRESSED); /*Be sure the focuses obj is not stuck in pressed state*/
221             lv_obj_scroll_to_view(dsc->focused_obj, LV_ANIM_OFF);
222         }
223     }
224     else if(code == LV_EVENT_DEFOCUSED) {
225         if(dsc->focused_obj) {
226             lv_obj_clear_state(dsc->focused_obj, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
227         }
228     }
229     else if(code == LV_EVENT_CHILD_CREATED) {
230         lv_obj_t * child = lv_event_get_target(e);
231         if(lv_obj_get_parent(child) == obj) {
232             if(dsc->focused_obj == NULL) {
233                 dsc->focused_obj = child;
234                 if(lv_obj_has_state(obj, LV_STATE_FOCUSED)) {
235                     lv_obj_add_state(child, LV_STATE_FOCUSED | LV_STATE_FOCUS_KEY);
236                     lv_obj_scroll_to_view(child, LV_ANIM_OFF);
237                 }
238             }
239         }
240     }
241     else if(code == LV_EVENT_CHILD_DELETED) {
242         /*This event bubble, so be sure this object's child was deleted.
243          *As we don't know which object was deleted we can't make the next focused.
244          *So make the first object focused*/
245         lv_obj_t * target = lv_event_get_target(e);
246         if(target == obj) {
247             dsc->focused_obj = find_first_focusable(obj);
248         }
249     }
250     else if(code == LV_EVENT_DELETE) {
251         lv_gridnav_remove(obj);
252     }
253     else if(code == LV_EVENT_PRESSED || code == LV_EVENT_PRESSING || code == LV_EVENT_PRESS_LOST ||
254             code == LV_EVENT_LONG_PRESSED || code == LV_EVENT_LONG_PRESSED_REPEAT ||
255             code == LV_EVENT_CLICKED || code == LV_EVENT_RELEASED) {
256         if(lv_group_get_focused(lv_obj_get_group(obj)) == obj) {
257             /*Forward press/release related event too*/
258             lv_indev_type_t t = lv_indev_get_type(lv_indev_get_act());
259             if(t == LV_INDEV_TYPE_ENCODER || t == LV_INDEV_TYPE_KEYPAD) {
260                 lv_event_send(dsc->focused_obj, code, lv_indev_get_act());
261             }
262         }
263     }
264 }
265 
find_chid(lv_obj_t * obj,lv_obj_t * start_child,find_mode_t mode)266 static lv_obj_t * find_chid(lv_obj_t * obj, lv_obj_t * start_child, find_mode_t mode)
267 {
268     lv_coord_t x_start = get_x_center(start_child);
269     lv_coord_t y_start = get_y_center(start_child);
270     uint32_t child_cnt = lv_obj_get_child_cnt(obj);
271     lv_obj_t * guess = NULL;
272     lv_coord_t x_err_guess = LV_COORD_MAX;
273     lv_coord_t y_err_guess = LV_COORD_MAX;
274     lv_coord_t h_half = lv_obj_get_height(start_child) / 2;
275     lv_coord_t h_max = lv_obj_get_height(obj) + lv_obj_get_scroll_top(obj) + lv_obj_get_scroll_bottom(obj);
276     uint32_t i;
277     for(i = 0; i < child_cnt; i++) {
278         lv_obj_t * child = lv_obj_get_child(obj, i);
279         if(child == start_child) continue;
280         if(obj_is_focuable(child) == false) continue;
281 
282         lv_coord_t x_err = 0;
283         lv_coord_t y_err = 0;
284         switch(mode) {
285             case FIND_LEFT:
286                 x_err = get_x_center(child) - x_start;
287                 y_err = get_y_center(child) - y_start;
288                 if(x_err >= 0) continue;    /*It's on the right*/
289                 if(LV_ABS(y_err) > h_half) continue;    /*Too far*/
290                 break;
291             case FIND_RIGHT:
292                 x_err = get_x_center(child) - x_start;
293                 y_err = get_y_center(child) - y_start;
294                 if(x_err <= 0) continue;    /*It's on the left*/
295                 if(LV_ABS(y_err) > h_half) continue;    /*Too far*/
296                 break;
297             case FIND_TOP:
298                 x_err = get_x_center(child) - x_start;
299                 y_err = get_y_center(child) - y_start;
300                 if(y_err >= 0) continue;    /*It's on the bottom*/
301                 break;
302             case FIND_BOTTOM:
303                 x_err = get_x_center(child) - x_start;
304                 y_err = get_y_center(child) - y_start;
305                 if(y_err <= 0) continue;    /*It's on the top*/
306                 break;
307             case FIND_NEXT_ROW_FIRST_ITEM:
308                 y_err = get_y_center(child) - y_start;
309                 if(y_err <= 0) continue;    /*It's on the top*/
310                 x_err = lv_obj_get_x(child);
311                 break;
312             case FIND_PREV_ROW_LAST_ITEM:
313                 y_err = get_y_center(child) - y_start;
314                 if(y_err >= 0) continue;    /*It's on the bottom*/
315                 x_err = obj->coords.x2 - child->coords.x2;
316                 break;
317             case FIND_FIRST_ROW:
318                 x_err = get_x_center(child) - x_start;
319                 y_err = lv_obj_get_y(child);
320                 break;
321             case FIND_LAST_ROW:
322                 x_err = get_x_center(child) - x_start;
323                 y_err = h_max - lv_obj_get_y(child);
324         }
325 
326         if(guess == NULL ||
327            (y_err * y_err + x_err * x_err < y_err_guess * y_err_guess + x_err_guess * x_err_guess)) {
328             guess = child;
329             x_err_guess  = x_err;
330             y_err_guess  = y_err;
331         }
332     }
333     return guess;
334 }
335 
find_first_focusable(lv_obj_t * obj)336 static lv_obj_t * find_first_focusable(lv_obj_t * obj)
337 {
338     uint32_t child_cnt = lv_obj_get_child_cnt(obj);
339     uint32_t i;
340     for(i = 0; i < child_cnt; i++) {
341         lv_obj_t * child = lv_obj_get_child(obj, i);
342         if(obj_is_focuable(child)) return child;
343 
344     }
345     return NULL;
346 }
347 
find_last_focusable(lv_obj_t * obj)348 static lv_obj_t * find_last_focusable(lv_obj_t * obj)
349 {
350     uint32_t child_cnt = lv_obj_get_child_cnt(obj);
351     int32_t i;
352     for(i = child_cnt - 1; i >= 0; i--) {
353         lv_obj_t * child = lv_obj_get_child(obj, i);
354         if(obj_is_focuable(child)) return child;
355     }
356     return NULL;
357 }
358 
obj_is_focuable(lv_obj_t * obj)359 static bool obj_is_focuable(lv_obj_t * obj)
360 {
361     if(lv_obj_has_flag(obj, LV_OBJ_FLAG_HIDDEN)) return false;
362     if(lv_obj_has_flag(obj, LV_OBJ_FLAG_CLICKABLE | LV_OBJ_FLAG_CLICK_FOCUSABLE)) return true;
363     else return false;
364 }
365 
get_x_center(lv_obj_t * obj)366 static lv_coord_t get_x_center(lv_obj_t * obj)
367 {
368     return obj->coords.x1 + lv_area_get_width(&obj->coords) / 2;
369 }
370 
get_y_center(lv_obj_t * obj)371 static lv_coord_t get_y_center(lv_obj_t * obj)
372 {
373     return obj->coords.y1 + lv_area_get_height(&obj->coords) / 2;
374 }
375 
376 #endif /*LV_USE_GRIDNAV*/
377