-
Notifications
You must be signed in to change notification settings - Fork 159
Expand file tree
/
Copy pathmenu_layer.c
More file actions
1429 lines (1224 loc) · 55 KB
/
menu_layer.c
File metadata and controls
1429 lines (1224 loc) · 55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/* SPDX-FileCopyrightText: 2024 Google LLC */
/* SPDX-License-Identifier: Apache-2.0 */
#include "menu_layer.h"
#include "menu_layer_private.h"
#include "applib/applib_malloc.auto.h"
#include "applib/preferred_content_size.h"
#include "applib/graphics/graphics.h"
#include "applib/graphics/text.h"
#include "util/trig.h"
#include "applib/fonts/fonts.h"
#include "applib/ui/animation_timing.h"
#include "applib/ui/click.h"
#include "applib/ui/window.h"
#include "applib/pbl_std/pbl_std.h"
#include "applib/legacy2/ui/menu_layer_legacy2.h"
#include "kernel/pbl_malloc.h"
#include "process_management/process_manager.h"
#include "shell/prefs.h"
#include "shell/system_theme.h"
#include "system/logging.h"
#include "system/passert.h"
#include "util/math.h"
#include "util/size.h"
#include "vibes.h"
#include <string.h>
//! @return True if there was an animation to cancel, false otherwise
static bool prv_cancel_selection_animation(MenuLayer *menu_layer);
//////////////////////
// Menu Layer
//
// NOTES: The MenuLayer is built on top of ScrollLayer. It uses ScrollLayer's scrolling and clipping features.
// Since it easily becomes to costly in terms of RAM to hold a layer for each row in the menu in memory,
// the MenuLayer does not use layers for its rows and headers. When a row is about to be displayed,
// it will call out to the client using a callback to get that row drawn.
// Inside the MenuLayer's update_proc (Layer drawing callback), it will call out to its client for each row
// that needs to be drawn, until all visible rows have been drawn.
static void prv_menu_scroll_offset_changed_handler(ScrollLayer *scroll_layer,
MenuLayer *menu_layer) {
// TODO: we might need to propagate this event down to MenuLayerCallbacks
}
static void prv_menu_select_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
// If the selection animation is running, complete it. Note that 2.x apps don't have a selection
// animation.
if (menu_layer->animation.animation) {
animation_set_elapsed(menu_layer->animation.animation,
animation_get_duration(menu_layer->animation.animation, true, true));
}
// If we're in the middle of scrolling, finish scrolling immediately before handling the select
// click. We do this to make a transition animation have a consistent position to animate from.
// Note that animation_set_elapsed isn't supported on 2.x animations. Just skip this step, as
// no 2.x transitions interact directly with menu layer state.
if (!process_manager_compiled_with_legacy2_sdk() && menu_layer->scroll_layer.animation) {
Animation *scroll_layer_animation =
property_animation_get_animation(menu_layer->scroll_layer.animation);
animation_set_elapsed(scroll_layer_animation,
animation_get_duration(scroll_layer_animation, true, true));
}
// Actually handle the click
if (menu_layer->callbacks.select_click) {
menu_layer->callbacks.select_click(menu_layer, &menu_layer->selection.index,
menu_layer->callback_context);
}
}
static void prv_menu_select_long_click_handler(ClickRecognizerRef recognizer,
MenuLayer *menu_layer) {
if (menu_layer->callbacks.select_long_click) {
menu_layer->callbacks.select_long_click(menu_layer, &menu_layer->selection.index,
menu_layer->callback_context);
}
}
static inline uint16_t prv_menu_layer_get_num_sections(MenuLayer *menu_layer);
static inline uint16_t prv_menu_layer_get_num_rows(MenuLayer *menu_layer, uint16_t section_index);
static bool prv_menu_index_is_first_index(MenuLayer *menu_layer, const MenuIndex *index) {
(void)menu_layer;
MenuIndex first_index = MenuIndex(0, 0);
return menu_index_compare(index, &first_index) == 0;
}
static bool prv_menu_index_is_last_index(MenuLayer *menu_layer, const MenuIndex *index) {
int last_index_section = prv_menu_layer_get_num_sections(menu_layer) - 1;
int last_index_row = prv_menu_layer_get_num_rows(menu_layer, last_index_section) - 1;
MenuIndex last_index = MenuIndex(last_index_section, last_index_row);
return menu_index_compare(index, &last_index) == 0;
}
static void prv_vibe_pulse(void) {
uint32_t const segments[] = { 50 };
VibePattern pat = {
.durations = segments,
.num_segments = ARRAY_LENGTH(segments),
};
vibes_enqueue_custom_pattern(pat);
}
//! Handle the menu scroll wrap around
//! @param menu_layer reference to the current MenuLayer
//! @param recognizer reference to the ClickRecognizer struct
//! @param scrolling_up `true` if scrolling up, `false` if scrolling down
//! @return `true` if a wrap around has been applied
static bool prv_menu_scroll_handle_wrap_around(MenuLayer *menu_layer, ClickRecognizerRef recognizer, bool scrolling_up) {
const uint8_t current_scroll_action = scrolling_up ? MenuLayerRepeatScrollingUp : MenuLayerRepeatScrollingDown;
const bool is_repeating = click_recognizer_is_repeating(recognizer);
if (is_repeating) {
menu_layer->cache.button_repeat_scrolling = current_scroll_action;
if (!menu_layer->scroll_force_wrap_on_repeat) {
return false;
}
}
menu_layer->cache.button_repeat_scrolling = MenuLayerNoRepeatScrolling;
MenuIndex current_index = menu_layer->selection.index;
int last_index_section = prv_menu_layer_get_num_sections(menu_layer) - 1;
int last_index_row = prv_menu_layer_get_num_rows(menu_layer, last_index_section) - 1;
MenuIndex first_index = MenuIndex(0, 0);
MenuIndex last_index = MenuIndex(last_index_section, last_index_row);
MenuIndex *wraparound_dest_index;
if ((menu_index_compare(¤t_index, &first_index) == 0) && scrolling_up) {
wraparound_dest_index = &last_index;
} else if ((menu_index_compare(¤t_index, &last_index) == 0) && !scrolling_up) {
wraparound_dest_index = &first_index;
} else {
return false;
}
const bool animated = true;
menu_layer_set_selected_index(menu_layer, *wraparound_dest_index, MenuRowAlignCenter, animated);
if (menu_layer->scroll_vibe_on_wrap_around) {
prv_vibe_pulse();
}
return true;
}
void menu_up_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
const bool up = true;
if (menu_layer->scroll_wrap_around && prv_menu_scroll_handle_wrap_around(menu_layer, recognizer, up)) {
return;
}
MenuIndex prev_index = menu_layer->selection.index;
const bool animated = true;
menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
MenuIndex current_index = menu_layer->selection.index;
if ((menu_layer->scroll_vibe_on_blocked) &&
(menu_index_compare(¤t_index, &prev_index) == 0) &&
(prv_menu_index_is_first_index(menu_layer, ¤t_index))) {
prv_vibe_pulse();
}
}
void menu_down_click_handler(ClickRecognizerRef recognizer, MenuLayer *menu_layer) {
const bool up = false;
if (menu_layer->scroll_wrap_around && prv_menu_scroll_handle_wrap_around(menu_layer, recognizer, up)) {
return;
}
MenuIndex prev_index = menu_layer->selection.index;
const bool animated = true;
menu_layer_set_selected_next(menu_layer, up, MenuRowAlignCenter, animated);
MenuIndex current_index = menu_layer->selection.index;
if ((menu_layer->scroll_vibe_on_blocked) &&
(menu_index_compare(¤t_index, &prev_index) == 0) &&
(prv_menu_index_is_last_index(menu_layer, ¤t_index))) {
prv_vibe_pulse();
}
}
static void prv_menu_click_config_provider(MenuLayer *menu_layer) {
// The config that gets passed in, has already the UP and DOWN buttons configured
// we're overriding the default behavior here:
window_single_repeating_click_subscribe(BUTTON_ID_UP, 100 /*ms*/,
(ClickHandler)menu_up_click_handler);
if (menu_layer->callbacks.select_click) {
window_single_click_subscribe(BUTTON_ID_SELECT, (ClickHandler)prv_menu_select_click_handler);
}
if (menu_layer->callbacks.select_long_click) {
window_long_click_subscribe(BUTTON_ID_SELECT, 0,
(ClickHandler)prv_menu_select_long_click_handler, NULL);
}
window_single_repeating_click_subscribe(BUTTON_ID_DOWN, 100 /*ms*/,
(ClickHandler)menu_down_click_handler);
}
static inline uint16_t prv_menu_layer_get_num_sections(MenuLayer *menu_layer) {
if (menu_layer->callbacks.get_num_sections) {
return menu_layer->callbacks.get_num_sections(menu_layer, menu_layer->callback_context);
} else {
return 1; // default
}
}
static inline uint16_t prv_menu_layer_get_num_rows(MenuLayer *menu_layer, uint16_t section_index) {
if (section_index == MENU_INDEX_NOT_FOUND) {
return 0;
}
if (menu_layer->callbacks.get_num_rows) {
return menu_layer->callbacks.get_num_rows(menu_layer, section_index,
menu_layer->callback_context);
} else {
return 1; // default
}
}
static inline int16_t prv_menu_layer_get_separator_height(MenuLayer *menu_layer,
MenuIndex *cell_index) {
if (menu_layer->callbacks.get_separator_height) {
return menu_layer->callbacks.get_separator_height(menu_layer, cell_index, menu_layer->callback_context);
} else if (process_manager_compiled_with_legacy2_sdk()) {
return MENU_CELL_LEGACY2_BASIC_SEPARATOR_HEIGHT;
} else {
return MENU_CELL_BASIC_SEPARATOR_HEIGHT;
}
}
static inline int16_t prv_menu_layer_get_header_height(MenuLayer *menu_layer,
uint16_t section_index) {
if (menu_layer->callbacks.get_header_height) {
return menu_layer->callbacks.get_header_height(menu_layer, section_index, menu_layer->callback_context);
} else {
return 0; // default
}
}
static inline int16_t prv_menu_layer_get_cell_height(MenuLayer *menu_layer, MenuIndex
*cell_index, bool provide_correct_selection_index) {
if (menu_layer->callbacks.get_cell_height) {
const MenuIndex prev_selection_index = menu_layer->selection.index;
if (!provide_correct_selection_index) {
menu_layer->selection.index.section = MENU_INDEX_NOT_FOUND;
}
const int16_t result = menu_layer->callbacks.get_cell_height(menu_layer, cell_index,
menu_layer->callback_context);
menu_layer->selection.index = prev_selection_index;
return result;
} else {
return menu_cell_basic_cell_height(); // default
}
}
static inline void prv_menu_layer_draw_separator(MenuLayer *menu_layer, Layer *cell_layer,
MenuCellSpan *cursor, GContext* ctx) {
const int16_t y = cursor->y - cursor->sep;
if (menu_layer->callbacks.draw_separator) {
// Save current drawing state:
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
GRect prev_bounds = cell_layer->bounds;
GRect new_bounds = prev_bounds;
// Translate the drawing_box to the bounds of the layer:
ctx->draw_state.drawing_box.origin.y += y;
ctx->draw_state.drawing_box.size.h = cursor->h;
// Set the height appropriately on the cell layer
new_bounds.size.h = cursor->sep;
layer_set_bounds(cell_layer, &new_bounds);
// Call the client, to ask to draw the separator:
menu_layer->callbacks.draw_separator(ctx, cell_layer, &cursor->index, menu_layer->callback_context);
// Restore current drawing state:
graphics_context_set_drawing_state(ctx, prev_state);
// Restore the layer bounds:
layer_set_bounds(cell_layer, &prev_bounds);
} else {
graphics_fill_rect(
ctx, &GRect(0, y, menu_layer->scroll_layer.layer.bounds.size.w, cursor->sep));
}
}
static void prv_prepare_row(GContext *ctx, MenuLayer *menu_layer,
Layer *cell_layer, bool highlight) {
if (!process_manager_compiled_with_legacy2_sdk()) {
GColor *colors = (highlight) ? menu_layer->highlight_colors : menu_layer->normal_colors;
ctx->draw_state.fill_color = colors[MenuLayerColorBackground];
ctx->draw_state.text_color = colors[MenuLayerColorForeground];
ctx->draw_state.tint_color = colors[MenuLayerColorForeground];
if (!gcolor_is_transparent(ctx->draw_state.fill_color)) {
graphics_fill_rect(ctx, &cell_layer->bounds);
}
}
cell_layer->is_highlighted = highlight;
}
static void prv_prepare_and_draw_row(GContext *ctx, MenuLayer *menu_layer,
Layer *cell_layer, MenuCellSpan *cursor, bool highlight) {
prv_prepare_row(ctx, menu_layer, cell_layer, highlight);
const GRect prev_bounds = cell_layer->bounds;
// in theory, we could decrement the origin by cell_content_origin_offset_y after the call
// in practice once shouldn't trust the draw_row implementation
const int16_t draw_box_origin_y = ctx->draw_state.drawing_box.origin.y;
ctx->draw_state.drawing_box.origin.y += menu_layer->animation.cell_content_origin_offset_y;
// Call the client, to ask to draw the row:
menu_layer->callbacks.draw_row(ctx, cell_layer, &cursor->index, menu_layer->callback_context);
ctx->draw_state.drawing_box.origin.y = draw_box_origin_y;
cell_layer->bounds = prev_bounds;
}
static inline void prv_menu_layer_draw_row(MenuLayer *menu_layer, Layer *cell_layer,
MenuCellSpan *cursor, GContext* ctx) {
if (cursor->h == 0) {
// cell has height 0, no need to draw anything.
return;
}
cell_layer->bounds.size.h = cursor->h;
cell_layer->frame.size.h = cursor->h;
cell_layer->frame.origin.y = cursor->y;
// Save current drawing state:
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
// Translate the drawing_box to the bounds of the layer:
ctx->draw_state.drawing_box.origin.y += cursor->y;
ctx->draw_state.drawing_box.size.h = cursor->h;
// Use the drawing_box as a clipper to force the content to only use
// the space available to it and remove overflow
const GRect *const rect_clipper = (const GRect *const)&ctx->draw_state.drawing_box;
grect_clip((GRect *const)&ctx->draw_state.clip_box, rect_clipper);
const bool fully_covered = grect_equal(&cell_layer->frame, &menu_layer->inverter.layer.frame);
const bool partial = grect_overlaps_grect(&cell_layer->frame, &menu_layer->inverter.layer.frame);
if (fully_covered || !partial) {
prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, fully_covered);
} else {
// Render the full cell without highlight
prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, false);
// Set clipper to the inverter layer in clipping box coordinates
GRect selection_clipper;
layer_get_global_frame(&menu_layer->inverter.layer, &selection_clipper);
grect_clip((GRect *const)&ctx->draw_state.clip_box, &selection_clipper);
// Render with highlight
prv_prepare_and_draw_row(ctx, menu_layer, cell_layer, cursor, true);
}
// Restore current drawing state:
graphics_context_set_drawing_state(ctx, prev_state);
}
static inline void prv_menu_layer_draw_section_header(MenuLayer *menu_layer, Layer *cell_layer,
MenuCellSpan *cursor, GContext* ctx) {
cell_layer->bounds.size.h = cursor->h;
cell_layer->frame.size.h = cursor->h;
cell_layer->frame.origin.y = cursor->y;
// Callback to get the shared cell instance filled with data:
// Save current drawing state:
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
// Translate the drawing_box to the bounds of the layer:
ctx->draw_state.drawing_box.origin.y += cursor->y;
ctx->draw_state.drawing_box.size.h = cursor->h;
const GRect *const rect_clipper = (const GRect *const)&ctx->draw_state.drawing_box;
grect_clip((GRect *const)&ctx->draw_state.clip_box, rect_clipper);
prv_prepare_row(ctx, menu_layer, cell_layer, false);
// Call the client, to ask to draw the section:
menu_layer->callbacks.draw_header(ctx, cell_layer, cursor->index.section, menu_layer->callback_context);
// Restore current drawing state:
graphics_context_set_drawing_state(ctx, prev_state);
}
static void prv_menu_layer_render_section_from_iterator(MenuIterator *iterator) {
MenuRenderIterator *it = (MenuRenderIterator*)iterator;
const int16_t top_diff = it->it.cursor.y - it->content_top_y;
const bool is_header_in_frame = (top_diff >= 0 && it->it.cursor.y <= it->content_bottom_y) ||
(it->it.cell_bottom_y >= it->content_top_y && it->it.cell_bottom_y <= it->content_bottom_y);
if (is_header_in_frame) {
// Draw section header:
prv_menu_layer_draw_section_header(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
// Draw the separator on top of the cell:
if (top_diff >= it->it.cursor.sep) {
prv_menu_layer_draw_separator(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
}
}
}
static void prv_menu_layer_render_row_from_iterator(MenuIterator *iterator) {
MenuRenderIterator *it = (MenuRenderIterator*)iterator;
const int16_t iter_y = it->it.cursor.y;
const int16_t top_diff = it->it.cursor.y - it->content_top_y;
const bool is_row_in_frame = (top_diff >= 0 && it->it.cursor.y <= it->content_bottom_y) ||
(it->it.cell_bottom_y >= it->content_top_y && it->it.cell_bottom_y <= it->content_bottom_y);
if (is_row_in_frame) {
it->cursor_in_frame = true;
// Draw the cell
prv_menu_layer_draw_row(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
// Draw the separator on top of the cell
if (top_diff >= it->it.cursor.sep) {
prv_menu_layer_draw_separator(it->it.menu_layer, &it->cell_layer, &it->it.cursor, it->ctx);
}
// Update the cache with the center-most row
it->it.cursor.y = iter_y;
if (false == it->cache_set) {
it->new_cache = it->it.cursor;
it->cache_set = true;
}
} else {
if (it->cursor_in_frame) {
it->it.should_continue = false;
}
}
it->it.cursor.y = iter_y;
}
// NOTE: The following two iteration functions are asymmetrical!
// In other words, even one is going downward and the other upward, there are some subtle
// differences. Most importantly: the downward function calls the row_callback_after_geometry for
// the row the iterator's cursor is currently set to, while the upward function skips over the
// current row.
// Secondly, section_callback is only called when a sections is encountered while walking.
// For example, if the current index is (section: 0, row: 0), the section_callback for section 0
// will only be called when walking upward.
static void prv_menu_layer_walk_downward_from_iterator(MenuIterator *it) {
const uint16_t num_sections = prv_menu_layer_get_num_sections(it->menu_layer);
it->should_continue = true;
for (;;) { // sections
const uint16_t num_rows_in_section = prv_menu_layer_get_num_rows(it->menu_layer,
it->cursor.index.section);
for (;;) { // rows
if (it->cursor.index.row >= num_rows_in_section) {
// Reached last row
break;
}
if (it->row_callback_before_geometry) {
it->row_callback_before_geometry(it);
}
it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, true);
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// ROW
if (it->row_callback_after_geometry) {
it->row_callback_after_geometry(it);
}
if (it->should_continue == false) {
return;
}
// Next row:
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
it->cursor.y = it->cell_bottom_y; // Bottom of previous cell is y of the next cell
// Don't leave space for the seperator for the (non-existent) row after the last row.
// This doesn't impact cell drawing in this loop (this condition will only trip on the last run).
// But, other parts of the system rely on the cursor being set properly at the end of this iteration.
if (it->cursor.index.row < num_rows_in_section - 1 || it->cursor.index.section < num_sections - 1) {
it->cursor.y += it->cursor.sep;
}
++(it->cursor.index.row);
} // for() rows
// Next section:
++(it->cursor.index.section);
if (it->cursor.index.section >= num_sections) {
break;
// Reached last section
}
it->cursor.index.row = 0;
it->cursor.h = prv_menu_layer_get_header_height(it->menu_layer, it->cursor.index.section);
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// SECTION
if (it->cursor.h > 0) {
it->section_callback(it);
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
it->cursor.y = it->cell_bottom_y + it->cursor.sep;
}
if (it->should_continue == false) {
return;
}
} // for() sections
}
static void prv_menu_layer_walk_upward_from_iterator(MenuIterator *it) {
it->should_continue = true;
for (;;) { // sections
for (;;) { // rows
// Previous row
if (it->cursor.index.row == 0) {
// Reached top-most row in current section
break;
}
--(it->cursor.index.row);
if (it->row_callback_before_geometry) {
it->row_callback_before_geometry(it);
}
// when walking upwards, selected_index isn't set yet here
// hence, the heights are the sizes as they were before the selection changed
it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, false);
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
it->cursor.y -= it->cursor.h + it->cursor.sep;
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// ask for height again, this time with correct selection status
it->cursor.h = prv_menu_layer_get_cell_height(it->menu_layer, &it->cursor.index, true);
// ROW
if (it->row_callback_after_geometry) {
it->row_callback_after_geometry(it);
}
if (it->should_continue == false) {
break;
}
} // for() rows
if (it->cursor.index.row == 0) {
// If top-most row, layout the section header
it->cursor.h = prv_menu_layer_get_header_height(it->menu_layer, it->cursor.index.section);
it->cursor.sep = prv_menu_layer_get_separator_height(it->menu_layer, &it->cursor.index);
if (it->cursor.h > 0) {
// Bottom of previous cell is y of the next cell
const int16_t total_height = it->cursor.h + it->cursor.sep;
if (total_height > it->cursor.y) {
// If the total height is greater than the cursor y, don't
// add in space to accodomate the separator as the downwards callback
// will add it for us.
it->cursor.y -= it->cursor.h;
} else {
it->cursor.y -= total_height;
}
it->cell_bottom_y = it->cursor.y + it->cursor.h;
// SECTION
it->section_callback(it);
}
}
if (it->should_continue == false) {
return;
}
// Previous section:
if (it->cursor.index.section == 0) {
// Reached top
break;
}
--(it->cursor.index.section);
// -1 will happen when entering for() rows
it->cursor.index.row = it->menu_layer->callbacks.get_num_rows(it->menu_layer,
it->cursor.index.section, it->menu_layer->callback_context);
} // for() sections
}
static void NOINLINE prv_draw_background(MenuLayer *menu_layer, GContext *ctx,
Layer *bg_layer, bool highlight) {
GDrawState prev_state = graphics_context_get_drawing_state(ctx);
const GRect *bounds = &bg_layer->bounds;
ctx->draw_state.drawing_box.origin.y = bounds->origin.y;
ctx->draw_state.drawing_box.size.h = bounds->size.h;
MenuLayerDrawBackgroundCallback draw_background_cb = menu_layer->callbacks.draw_background;
if (draw_background_cb) {
draw_background_cb(ctx, bg_layer, false, menu_layer->callback_context);
} else if (highlight) {
ctx->draw_state.fill_color = menu_layer->highlight_colors[MenuLayerColorBackground];
graphics_fill_rect(ctx, bounds);
} else {
ctx->draw_state.fill_color = menu_layer->normal_colors[MenuLayerColorBackground];
graphics_fill_rect(ctx, bounds);
}
graphics_context_set_drawing_state(ctx, prev_state);
}
void menu_layer_update_proc(Layer *scroll_content_layer, GContext* ctx) {
MenuLayer *menu_layer = (MenuLayer*)(((uint8_t*)scroll_content_layer) -
offsetof(MenuLayer, scroll_layer.content_sublayer));
const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
const int16_t content_top_y = -scroll_layer_get_content_offset(&menu_layer->scroll_layer).y;
const int16_t content_bottom_y = content_top_y + frame_size.h;
if (!process_manager_compiled_with_legacy2_sdk()) {
prv_draw_background(menu_layer, ctx, &menu_layer->scroll_layer.layer, false);
}
MenuRenderIterator *render_iter = applib_type_malloc(MenuRenderIterator);
PBL_ASSERTN(render_iter);
if (menu_layer->center_focused) {
// in this mode, the selected row is always the best candidate for the cache
menu_layer->cache.cursor = menu_layer->selection;
}
*render_iter = (MenuRenderIterator) {
.it = {
.menu_layer = menu_layer,
.cursor = menu_layer->cache.cursor,
.row_callback_after_geometry = prv_menu_layer_render_row_from_iterator,
.section_callback = prv_menu_layer_render_section_from_iterator,
},
.ctx = ctx,
.content_top_y = content_top_y,
.content_bottom_y = content_bottom_y,
.cache_set = false,
.cursor_in_frame = false,
.cell_layer = {
.bounds = {
.size = {
.w = frame_size.w,
},
},
.frame = {
.size = {
.w = frame_size.w,
},
},
},
};
layer_add_child(&menu_layer->scroll_layer.content_sublayer, &render_iter->cell_layer);
// Set separator color
graphics_context_set_fill_color(ctx, GColorBlack);
// We're caching the y-coord and index of the one row, as our "anchor" point in the menu.
// We'll be walking downward and upward from that index until the rows fall off the screen.
const int16_t content_center_y = (content_top_y + content_bottom_y) / 2;
if (content_center_y >= menu_layer->cache.cursor.y) {
// Walk downward from cache.cursor, then upward
prv_menu_layer_walk_downward_from_iterator(&render_iter->it);
render_iter->it.cursor = menu_layer->cache.cursor;
prv_menu_layer_walk_upward_from_iterator(&render_iter->it);
} else {
// Walk upward from cache.cursor, then downward
prv_menu_layer_walk_upward_from_iterator(&render_iter->it);
render_iter->it.cursor = menu_layer->cache.cursor;
prv_menu_layer_walk_downward_from_iterator(&render_iter->it);
}
layer_remove_from_parent(&render_iter->cell_layer);
// Assign the new cache:
menu_layer->cache.cursor = render_iter->new_cache;
task_free(render_iter);
}
void menu_layer_init_scroll_layer_callbacks(MenuLayer *menu_layer) {
ScrollLayer *scroll_layer = &menu_layer->scroll_layer;
scroll_layer_set_callbacks(scroll_layer, (ScrollLayerCallbacks) {
.click_config_provider = (ClickConfigProvider)prv_menu_click_config_provider,
.content_offset_changed_handler = (ScrollLayerCallback)prv_menu_scroll_offset_changed_handler,
});
scroll_layer->content_sublayer.update_proc = (LayerUpdateProc)menu_layer_update_proc;
}
static void prv_set_center_focused(MenuLayer *menu_layer, bool center_focused) {
menu_layer->center_focused = center_focused;
scroll_layer_set_clips_content_offset(&menu_layer->scroll_layer, !center_focused);
}
void menu_layer_init(MenuLayer *menu_layer, const GRect *frame) {
*menu_layer = (MenuLayer) {
.pad_bottom = true,
};
ScrollLayer *scroll_layer = &menu_layer->scroll_layer;
scroll_layer_init(scroll_layer, frame);
menu_layer_init_scroll_layer_callbacks(menu_layer);
scroll_layer_set_shadow_hidden(scroll_layer, true);
scroll_layer_set_context(scroll_layer, menu_layer);
menu_layer_set_normal_colors(menu_layer, system_theme_get_bg_color(), system_theme_get_fg_color());
GColor highlight_bg = shell_prefs_get_theme_highlight_color();
menu_layer_set_highlight_colors(menu_layer, highlight_bg, gcolor_legible_over(highlight_bg));
InverterLayer *inverter = &menu_layer->inverter;
inverter_layer_init(inverter, &GRectZero);
scroll_layer_add_child(scroll_layer, &inverter->layer);
// Hide inverter layer by default for 3.0 apps
layer_set_hidden(inverter_layer_get_layer(&menu_layer->inverter), true);
#if PBL_ROUND
prv_set_center_focused(menu_layer, true);
#endif
}
MenuLayer* menu_layer_create(GRect frame) {
MenuLayer *layer = applib_type_malloc(MenuLayer);
if (layer) {
menu_layer_init(layer, &frame);
}
return layer;
}
void menu_layer_pad_bottom_enable(MenuLayer *menu_layer, bool enable) {
menu_layer->pad_bottom = enable;
}
void menu_layer_deinit(MenuLayer *menu_layer) {
prv_cancel_selection_animation(menu_layer);
layer_deinit(&menu_layer->inverter.layer);
scroll_layer_deinit(&menu_layer->scroll_layer);
}
void menu_layer_destroy(MenuLayer* menu_layer) {
if (menu_layer == NULL) {
return;
}
menu_layer_deinit(menu_layer);
applib_free(menu_layer);
}
Layer* menu_layer_get_layer(const MenuLayer *menu_layer) {
return &((MenuLayer *)menu_layer)->scroll_layer.layer;
}
ScrollLayer* menu_layer_get_scroll_layer(const MenuLayer *menu_layer) {
return &((MenuLayer *)menu_layer)->scroll_layer;
}
typedef struct MenuPrimeCacheIterator {
MenuIterator it;
bool cache_set;
} MenuPrimeCacheIterator;
static void prv_menu_layer_iterator_noop_callback(MenuIterator *it) {
(void)it;
}
static void prv_menu_layer_iterator_prime_cache_callback(MenuIterator *iterator) {
MenuPrimeCacheIterator *it = (MenuPrimeCacheIterator*)iterator;
if (false == it->cache_set) {
// Prime the cursor cache:
it->it.menu_layer->cache.cursor = it->it.cursor;
// Set initial selection too:
it->it.menu_layer->selection = it->it.cursor;
it->cache_set = true;
}
}
//! Calculate the total height of all row cells and section headers,
//! and assign the appropriate content size to the scroll_layer.
//! Also prime the offset cache on the fly.
void menu_layer_update_caches(MenuLayer *menu_layer) {
// Save the currently selected cell index.
MenuIndex selected_index = menu_layer_get_selected_index(menu_layer);
MenuPrimeCacheIterator it = {
.it = {
.menu_layer = menu_layer,
.row_callback_after_geometry = prv_menu_layer_iterator_prime_cache_callback,
.section_callback = prv_menu_layer_iterator_noop_callback,
.should_continue = true,
.cursor = {
// Section header of current section (0) is not part of the walk down, set it "manually"
.y = prv_menu_layer_get_header_height(menu_layer, 0),
.sep = prv_menu_layer_get_separator_height(menu_layer, 0)
},
},
.cache_set = false,
};
if (prv_menu_layer_get_header_height(menu_layer, 0) != 0) {
// We have to add the separator height, as when drawing down -> up, we render the separator
// for the row above before proceeding down. We only render this separator at the top if we
// have headers on the first section.
it.it.cursor.y += it.it.cursor.sep;
}
// handle special case of just one row so that calls for menu_layer_get_selected_index()
// will already answer correctly
if (prv_menu_layer_get_num_sections(menu_layer) == 1 &&
prv_menu_layer_get_num_rows(menu_layer, 0) == 1) {
menu_layer->selection.index = MenuIndex(0, 0);
}
prv_menu_layer_walk_downward_from_iterator(&it.it);
int16_t total_height = it.it.cursor.y;
if (menu_layer->pad_bottom) {
total_height += MENU_LAYER_BOTTOM_PADDING;
}
// Set the content size on the scroll layer, so all the rows will fit onto the content layer:
const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
scroll_layer_set_content_size(&menu_layer->scroll_layer, GSize(frame_size.w, total_height));
// Set the selected cell again:
const bool animated = false;
menu_layer_set_selected_index(menu_layer, selected_index, MenuRowAlignNone, animated);
}
void menu_layer_set_callbacks(MenuLayer *menu_layer, void *callback_context,
const MenuLayerCallbacks *callbacks) {
if (callbacks) {
menu_layer->callbacks = *callbacks;
PBL_ASSERTN(menu_layer->callbacks.draw_row);
PBL_ASSERTN(menu_layer->callbacks.get_num_rows);
}
menu_layer->callback_context = callback_context;
menu_layer_reload_data(menu_layer);
}
void menu_layer_set_callbacks_by_value(MenuLayer *menu_layer, void *callback_context,
MenuLayerCallbacks callbacks) {
menu_layer_set_callbacks(menu_layer, callback_context, &callbacks);
}
void menu_layer_set_click_config_onto_window(MenuLayer *menu_layer, struct Window *window) {
// Delegate this directly to the scroll layer:
scroll_layer_set_click_config_onto_window(&menu_layer->scroll_layer, window);
}
//! @returns 0 if A and B are equal, 1 if A has a higher section & row combination than B or else -1
int16_t menu_index_compare(const MenuIndex *a, const MenuIndex *b) {
const int16_t max_rows = MAX(a->row, b->row) + 1;
const int32_t a_abs = ((a->section * max_rows) + a->row);
const int32_t b_abs = ((b->section * max_rows) + b->row);
if (a_abs > b_abs) {
return 1;
} else if (a_abs < b_abs) {
return -1;
} else {
return 0;
}
}
static void prv_selection_complete(Animation *animation, bool finished, void *context) {
MenuLayer *menu_layer = (MenuLayer *) context;
menu_layer->animation.animation = NULL;
}
static bool prv_cancel_selection_animation(MenuLayer *menu_layer) {
const bool result = animation_is_scheduled(menu_layer->animation.animation);
if (result) {
animation_unschedule(menu_layer->animation.animation);
}
menu_layer->animation.animation = NULL;
return result;
}
#define TOP_DOWN_PX 7
#define BOTTOM_DOWN_PX 10
static void prv_setup_selection_animation(MenuLayer *menu_layer, bool up) {
// Move selection inverter layer:
const int16_t w = menu_layer->scroll_layer.layer.frame.size.w;
const GSize size = GSize(w, menu_layer->selection.h);
// Step 1. Bring down TOP of cell by TOP_DOWN_PX.
GRect from;
if (menu_layer->animation.animation) {
from = menu_layer->animation.target;
prv_cancel_selection_animation(menu_layer);
} else {
from = menu_layer->inverter.layer.frame;
}
GRect target = (GRect) {
.origin = {
.x = 0,
.y = from.origin.y + ((up) ? 0 : TOP_DOWN_PX),
},
.size = {
.w = size.w,
.h = size.h - TOP_DOWN_PX,
}
};
Animation *a1 = (Animation *) property_animation_create_layer_frame(&menu_layer->inverter.layer,
&from, &target);
animation_set_duration(a1, 100);
animation_set_curve(a1, AnimationCurveEaseOut);
animation_set_auto_destroy(a1, true);
// Step 2. Skip the top of the highlight down to the top of the newly selected cell,
// and have the selection BOTTOM_DOWN_PX below the selected cell.
from.origin.y = menu_layer->selection.y - ((up) ? BOTTOM_DOWN_PX : 0);
from.size.h = size.h + BOTTOM_DOWN_PX;
// Step 3. Bring up the bottom of the highlight to only cover the selected cell.
target.origin.y = menu_layer->selection.y;
target.size = size;
Animation *a2 = (Animation *) property_animation_create_layer_frame(&menu_layer->inverter.layer,
&from, &target);
animation_set_duration(a2, 250);
animation_set_curve(a2, AnimationCurveEaseOut);
animation_set_auto_destroy(a2, true);
Animation *a = animation_sequence_create(a1, a2, NULL);
animation_set_auto_destroy(a, true); // [MJ] false?
animation_set_handlers(a, (AnimationHandlers) { .stopped = prv_selection_complete }, menu_layer);
menu_layer->animation.animation = a;
menu_layer->animation.target = target;
animation_schedule(a);
}
static void prv_menu_layer_update_selection_highlight(MenuLayer *menu_layer, bool up,
bool animated,
bool change_ongoing_animation) {
if (menu_layer->center_focused || menu_layer->selection_animation_disabled) {
// animation on center_focused will not happen by moving the selection
// see prv_schedule_center_focus_animation()
animated = false;
}
Animation *scroll_animation = (Animation *) menu_layer->scroll_layer.animation;
if (change_ongoing_animation && animation_is_scheduled(scroll_animation)) {
animation_unschedule(scroll_animation);
}
if (change_ongoing_animation && animated && !process_manager_compiled_with_legacy2_sdk()) {
prv_setup_selection_animation(menu_layer, up);
} else {
if (change_ongoing_animation) {
prv_cancel_selection_animation(menu_layer);
}
// Move selection inverter layer:
const int16_t w = menu_layer->scroll_layer.layer.frame.size.w;
const GSize size = GSize(w, menu_layer->selection.h);
menu_layer->inverter.layer.bounds = (GRect) {
.origin = { 0, 0 },
.size = size,
};
menu_layer->inverter.layer.frame = (GRect) {
.origin = {
.x = 0,
.y = menu_layer->selection.y,
},
.size = size,
};
layer_mark_dirty(&menu_layer->inverter.layer);
}
}
static MenuRowAlign prv_corrected_scroll_align(MenuLayer *menu_layer, MenuRowAlign align) {
if (menu_layer->center_focused) {
return MenuRowAlignCenter;
}
return align;
}
static void prv_menu_layer_update_selection_scroll_position(MenuLayer *menu_layer,
MenuRowAlign scroll_align,
bool animated) {
scroll_align = prv_corrected_scroll_align(menu_layer, scroll_align);
if (scroll_align != MenuRowAlignNone) {
int16_t y;
const GSize frame_size = menu_layer->scroll_layer.layer.frame.size;
// Scroll to the right position:
switch (scroll_align) {
case MenuRowAlignTop:
y = - menu_layer->selection.y;
break;
case MenuRowAlignBottom:
y = frame_size.h - menu_layer->selection.y - menu_layer->selection.h;
break;
default:
case MenuRowAlignCenter:
y = (frame_size.h / 2) - menu_layer->selection.y - (menu_layer->selection.h / 2);
break;
}
if (menu_layer->center_focused) {
// animation on center_focus will not happen via scrolling
// see prv_schedule_center_focus_animation()
animated = false;
}