1// MIT License
2
3// Copyright (c) 2017 Vadim Grigoruk @nesbox // grigoruk@gmail.com
4
5// Permission is hereby granted, free of charge, to any person obtaining a copy
6// of this software and associated documentation files (the "Software"), to deal
7// in the Software without restriction, including without limitation the rights
8// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9// copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11
12// The above copyright notice and this permission notice shall be included in all
13// copies or substantial portions of the Software.
14
15// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21// SOFTWARE.
22
23#include "menu.h"
24#include "studio/studio.h"
25#include "studio/fs.h"
26
27#include <math.h>
28
29#define ANIM_STATES(macro) \
30 macro(idle) \
31 macro(start) \
32 macro(up) \
33 macro(down) \
34 macro(close) \
35 macro(back)
36
37#define ANIM_MOVIE(name) Movie name;
38#define ANIM_FREE(name) FREE(menu->anim.name.items);
39
40struct Menu
41{
42 Studio* studio;
43 tic_mem* tic;
44
45 s32 ticks;
46
47 MenuItem* items;
48 s32 count;
49 s32 pos;
50 s32 backPos;
51
52 void* data;
53 MenuItemHandler back;
54
55 struct
56 {
57 s32 pos;
58 s32 top;
59 s32 bottom;
60 s32 cursor;
61 s32 offset;
62
63 Movie* movie;
64
65 ANIM_STATES(ANIM_MOVIE)
66
67 } anim;
68
69 struct
70 {
71 s32 item;
72 s32 option;
73 } maxwidth;
74};
75
76#define BG_ANIM_COLOR tic_color_dark_grey
77
78enum
79{
80 Up, Down, Left, Right, A, B, X, Y
81};
82
83enum{TextMargin = 2, ItemHeight = TIC_FONT_HEIGHT + TextMargin * 2};
84enum{Hold = KEYBOARD_HOLD, Period = ItemHeight};
85
86static void emptyDone(void* data) {}
87
88static void menuUpDone(void* data)
89{
90 Menu *menu = data;
91 menu->pos = (menu->pos + (menu->count - 1)) % menu->count;
92 menu->anim.pos = 0;
93 menu->anim.movie = resetMovie(&menu->anim.idle);
94}
95
96static void menuDownDone(void* data)
97{
98 Menu *menu = data;
99 menu->pos = (menu->pos + (menu->count + 1)) % menu->count;
100 menu->anim.pos = 0;
101 menu->anim.movie = resetMovie(&menu->anim.idle);
102}
103
104static void startDone(void* data)
105{
106 Menu *menu = data;
107 menu->anim.movie = resetMovie(&menu->anim.idle);
108}
109
110static void closeDone(void* data)
111{
112 Menu *menu = data;
113 menu->anim.movie = resetMovie(&menu->anim.idle);
114 menu->items[menu->pos].handler(menu->data, menu->pos);
115}
116
117static void backDone(void* data)
118{
119 Menu *menu = data;
120 menu->anim.movie = resetMovie(&menu->anim.idle);
121 s32 pos = menu->backPos;
122 menu->back(menu->data, 0);
123 menu->pos = pos;
124}
125
126static void printShadow(tic_mem* tic, const char* text, s32 x, s32 y, tic_color color)
127{
128 tic_api_print(tic, text, x, y + 1, tic_color_black, true, 1, false);
129 tic_api_print(tic, text, x, y, color, true, 1, false);
130}
131
132static inline bool animIdle(Menu* menu)
133{
134 return menu->anim.movie == &menu->anim.idle;
135}
136
137static void drawTopBar(Menu* menu, s32 x, s32 y)
138{
139 tic_mem* tic = menu->tic;
140
141 y += menu->anim.top;
142
143 tic_api_rect(tic, x, y, TIC80_WIDTH, ItemHeight, tic_color_grey);
144 tic_api_rect(tic, x, y + ItemHeight, TIC80_WIDTH, 1, tic_color_black);
145
146 static const char Text[] = TIC_NAME_FULL;
147 printShadow(tic, Text, (TIC80_WIDTH - STRLEN(Text) * TIC_FONT_WIDTH) / 2, y + TextMargin, tic_color_white);
148}
149
150static void drawBottomBar(Menu* menu, s32 x, s32 y)
151{
152 tic_mem* tic = menu->tic;
153
154 y += menu->anim.bottom;
155
156 tic_api_rect(tic, x, y, TIC80_WIDTH, ItemHeight, tic_color_grey);
157 tic_api_rect(tic, x, y - 1, TIC80_WIDTH, 1, tic_color_black);
158
159 const char* help = menu->items[menu->pos].help;
160 if(help)
161 {
162 if(menu->ticks % TIC80_FRAMERATE < TIC80_FRAMERATE / 2)
163 printShadow(tic, help, x + (TIC80_WIDTH - strlen(help) * TIC_FONT_WIDTH) / 2 + menu->anim.offset,
164 y + TextMargin, tic_color_white);
165 }
166 else
167 {
168 static const char Text[] = TIC_COPYRIGHT;
169 printShadow(tic, Text, (TIC80_WIDTH - STRLEN(Text) * TIC_FONT_WIDTH) / 2, y + TextMargin, tic_color_white);
170 }
171}
172
173static void updateOption(MenuOption* option, s32 val, void* data)
174{
175 option->pos = (option->pos + option->count + val) % option->count;
176 option->set(data, option->pos);
177 option->pos = option->get(data);
178}
179
180static void onMenuItem(Menu* menu, const MenuItem* item)
181{
182 playSystemSfx(menu->studio, 2);
183 menu->anim.movie = resetMovie(item->back ? &menu->anim.back : &menu->anim.close);
184}
185
186static void drawOptionArrow(Menu* menu, MenuOption* option, s32 x, s32 y, s32 icon, s32 delta)
187{
188 tic_rect left = {x - 1, y, TIC_FONT_WIDTH, TIC_FONT_HEIGHT};
189 bool down = false;
190 bool over = false;
191 if(checkMousePos(menu->studio, &left))
192 {
193 over = true;
194 setCursor(menu->studio, tic_cursor_hand);
195 down = checkMouseDown(menu->studio, &left, tic_mouse_left);
196
197 if(checkMouseClick(menu->studio, &left, tic_mouse_left))
198 {
199 playSystemSfx(menu->studio, 2);
200 updateOption(option, delta, menu->data);
201 }
202 }
203
204 if(down)
205 {
206 drawBitIcon(menu->studio, icon, left.x, left.y, tic_color_white);
207 }
208 else
209 {
210 drawBitIcon(menu->studio, icon, left.x, left.y, tic_color_black);
211 drawBitIcon(menu->studio, icon, left.x, left.y - 1, over ? tic_color_white : tic_color_light_grey);
212 }
213}
214
215static void drawMenu(Menu* menu, s32 x, s32 y)
216{
217 if (getStudioMode(menu->studio) != TIC_MENU_MODE)
218 return;
219
220 tic_mem* tic = menu->tic;
221
222 if(animIdle(menu))
223 {
224 if(tic_api_btnp(menu->tic, Up, Hold, Period)
225 || tic_api_keyp(tic, tic_key_up, Hold, Period))
226 {
227 playSystemSfx(menu->studio, 2);
228 menu->anim.movie = resetMovie(&menu->anim.up);
229 }
230
231 if(tic_api_btnp(menu->tic, Down, Hold, Period)
232 || tic_api_keyp(tic, tic_key_down, Hold, Period))
233 {
234 playSystemSfx(menu->studio, 2);
235 menu->anim.movie = resetMovie(&menu->anim.down);
236 }
237
238 MenuItem* item = &menu->items[menu->pos];
239 MenuOption* option = item->option;
240 if(option)
241 {
242 if(tic_api_btnp(menu->tic, Left, Hold, Period)
243 || tic_api_keyp(tic, tic_key_left, Hold, Period))
244 {
245 playSystemSfx(menu->studio, 2);
246 updateOption(option, -1, menu->data);
247 }
248
249 if(tic_api_btnp(menu->tic, Right, Hold, Period)
250 || tic_api_keyp(tic, tic_key_right, Hold, Period))
251 {
252 playSystemSfx(menu->studio, 2);
253 updateOption(option, +1, menu->data);
254 }
255 }
256
257 if(tic_api_btnp(menu->tic, A, -1, -1)
258 || tic_api_keyp(tic, tic_key_return, Hold, Period))
259 {
260 if(option)
261 {
262 playSystemSfx(menu->studio, 2);
263 updateOption(option, +1, menu->data);
264 }
265 else if(menu->items[menu->pos].handler)
266 onMenuItem(menu, item);
267 }
268
269 if((tic_api_btnp(menu->tic, B, -1, -1)
270 || tic_api_keyp(tic, tic_key_backspace, Hold, Period))
271 && menu->back)
272 {
273 playSystemSfx(menu->studio, 2);
274 menu->anim.movie = resetMovie(&menu->anim.back);
275 }
276 }
277
278 s32 i = 0;
279 for(const MenuItem *it = menu->items, *end = it + menu->count; it != end; ++it, ++i)
280 {
281 s32 width = it->option ? menu->maxwidth.item + menu->maxwidth.option + 3 * TIC_FONT_WIDTH : it->width;
282
283 tic_rect rect = {x + (TIC80_WIDTH - width) / 2 + menu->anim.offset,
284 y + TextMargin + ItemHeight * (i - menu->pos) - menu->anim.pos, it->width, TIC_FONT_HEIGHT};
285
286 bool down = false;
287 if(animIdle(menu) && checkMousePos(menu->studio, &rect) && it->handler)
288 {
289 setCursor(menu->studio, tic_cursor_hand);
290
291 if(checkMouseDown(menu->studio, &rect, tic_mouse_left))
292 down = true;
293
294 if(checkMouseClick(menu->studio, &rect, tic_mouse_left))
295 {
296 if(it->handler)
297 {
298 menu->pos = i;
299 onMenuItem(menu, it);
300 }
301 }
302 }
303
304 if(down)
305 tic_api_print(tic, it->label, rect.x, rect.y + 1, tic_color_white, true, 1, false);
306 else
307 printShadow(tic, it->label, rect.x, rect.y, tic_color_white);
308
309 if(it->option)
310 {
311 drawOptionArrow(menu, it->option, rect.x + menu->maxwidth.item + TIC_FONT_WIDTH, rect.y, tic_icon_left, -1);
312 drawOptionArrow(menu, it->option,
313 rect.x + menu->maxwidth.item + it->option->width + 2 * TIC_FONT_WIDTH, rect.y, tic_icon_right, +1);
314
315 printShadow(tic, it->option->values[it->option->pos],
316 rect.x + menu->maxwidth.item + 2 * TIC_FONT_WIDTH, rect.y, tic_color_yellow);
317 }
318 }
319}
320
321// BG animation based on DevEd code
322void studio_menu_anim(tic_mem* tic, s32 ticks)
323{
324 tic_api_cls(tic, TIC_COLOR_BG);
325
326 enum{Gap = 72};
327
328 for(s32 x = 16; x <= 16 * 16; x += 16)
329 {
330 s32 ly = Gap - 8 * 32 * 16 / (x - ticks % 16);
331
332 tic_api_line(tic, 0, ly, TIC80_WIDTH, ly, BG_ANIM_COLOR);
333 tic_api_line(tic, 0, TIC80_HEIGHT - ly,
334 TIC80_WIDTH, TIC80_HEIGHT - ly, BG_ANIM_COLOR);
335 }
336
337 for(s32 x = -32; x <= 32; x++)
338 {
339 tic_api_line(tic, TIC80_WIDTH / 2 - x * 4, Gap - 16,
340 TIC80_WIDTH / 2 - x * 24, -16, BG_ANIM_COLOR);
341
342 tic_api_line(tic, TIC80_WIDTH / 2 - x * 4, TIC80_HEIGHT - Gap + 16,
343 TIC80_WIDTH / 2 - x * 24, TIC80_HEIGHT + 16, BG_ANIM_COLOR);
344 }
345}
346
347void studio_menu_anim_scanline(tic_mem* tic, s32 row, void* data)
348{
349 s32 dir = row < TIC80_HEIGHT / 2 ? 1 : -1;
350 s32 val = dir * (TIC80_WIDTH - row * 7 / 2);
351 tic_rgb* dst = tic->ram->vram.palette.colors + BG_ANIM_COLOR;
352
353 memcpy(dst, &(tic_rgb){val * 3 / 4, val * 4 / 5, val}, sizeof(tic_rgb));
354}
355
356static void drawCursor(Menu* menu, s32 x, s32 y)
357{
358 tic_mem* tic = menu->tic;
359
360 tic_api_rect(tic, x, y - (menu->anim.cursor - ItemHeight) / 2, TIC80_WIDTH, menu->anim.cursor, tic_color_red);
361}
362
363Menu* studio_menu_create(Studio* studio)
364{
365 Menu* menu = malloc(sizeof(Menu));
366 *menu = (Menu)
367 {
368 .studio = studio,
369 .tic = getMemory(studio),
370 .anim =
371 {
372 .idle = {.done = emptyDone,},
373
374 .start = MOVIE_DEF(10, startDone,
375 {
376 {-10, 0, 10, &menu->anim.top, AnimLinear},
377 {10, 0, 10, &menu->anim.bottom, AnimLinear},
378 {0, 10, 10, &menu->anim.cursor, AnimLinear},
379 {-TIC80_WIDTH, 0, 10, &menu->anim.offset, AnimLinear},
380 }),
381
382 .up = MOVIE_DEF(9, menuUpDone, {{0, -9, 9, &menu->anim.pos, AnimEaseIn}}),
383 .down = MOVIE_DEF(9, menuDownDone, {{0, 9, 9, &menu->anim.pos, AnimEaseIn}}),
384
385 .close = MOVIE_DEF(10, closeDone,
386 {
387 {0, -10, 10, &menu->anim.top, AnimLinear},
388 {0, 10, 10, &menu->anim.bottom, AnimLinear},
389 {10, 0, 10, &menu->anim.cursor, AnimLinear},
390 {0, TIC80_WIDTH, 10, &menu->anim.offset, AnimLinear},
391 }),
392
393 .back = MOVIE_DEF(10, backDone,
394 {
395 {0, -10, 10, &menu->anim.top, AnimLinear},
396 {0, 10, 10, &menu->anim.bottom, AnimLinear},
397 {10, 0, 10, &menu->anim.cursor, AnimLinear},
398 {0, TIC80_WIDTH, 10, &menu->anim.offset, AnimLinear},
399 }),
400 },
401 };
402
403 return menu;
404}
405
406#undef MOVIE_DEF
407
408void studio_menu_tick(Menu* menu)
409{
410 tic_mem* tic = menu->tic;
411
412 processAnim(menu->anim.movie, menu);
413
414 // process scroll
415 if(animIdle(menu))
416 {
417 tic80_input* input = &tic->ram->input;
418
419 if(input->mouse.scrolly)
420 {
421 if(tic_api_key(tic, tic_key_ctrl) || tic_api_key(tic, tic_key_shift))
422 {
423 MenuOption* option = menu->items[menu->pos].option;
424 if(option)
425 updateOption(option, input->mouse.scrolly < 0 ? -1 : +1, menu->data);
426 }
427 else
428 {
429 s32 pos = menu->pos + (input->mouse.scrolly < 0 ? +1 : -1);
430 menu->pos = CLAMP(pos, 0, menu->count - 1);
431 }
432 }
433 }
434
435 if(getStudioMode(menu->studio) != TIC_MENU_MODE)
436 return;
437
438 studio_menu_anim(tic, menu->ticks);
439
440 VBANK(tic, 1)
441 {
442 tic_api_cls(tic, tic->ram->vram.vars.clear = tic_color_blue);
443 memcpy(tic->ram->vram.palette.data, getConfig(menu->studio)->cart->bank0.palette.vbank0.data, sizeof(tic_palette));
444
445 drawCursor(menu, 0, (TIC80_HEIGHT - ItemHeight) / 2);
446 drawMenu(menu, 0, (TIC80_HEIGHT - ItemHeight) / 2);
447 drawTopBar(menu, 0, 0);
448 drawBottomBar(menu, 0, TIC80_HEIGHT - ItemHeight);
449 }
450
451 menu->ticks++;
452}
453
454void studio_menu_init(Menu* menu, const MenuItem* items, s32 rows, s32 pos, s32 backPos, MenuItemHandler back, void* data)
455{
456 const s32 size = sizeof menu->items[0] * rows;
457
458 *menu = (Menu)
459 {
460 .studio = menu->studio,
461 .tic = menu->tic,
462 .anim = menu->anim,
463 .items = realloc(menu->items, size),
464 .data = data,
465 .count = rows,
466 .pos = pos,
467 .backPos = backPos,
468 .back = back,
469 };
470
471 memcpy(menu->items, items, size);
472 for(MenuItem *it = menu->items, *end = it + menu->count; it != end; ++it)
473 {
474 it->width = strlen(it->label) * TIC_FONT_WIDTH;
475
476 if(it->option)
477 {
478 if(menu->maxwidth.item < it->width)
479 menu->maxwidth.item = it->width;
480
481 for(const char **opt = it->option->values, **end = opt + it->option->count; opt != end; ++opt)
482 {
483 s32 len = strlen(*opt) * TIC_FONT_WIDTH;
484
485 if(it->option->width < len)
486 it->option->width = len;
487
488 if(menu->maxwidth.option < len)
489 menu->maxwidth.option = len;
490 }
491
492 it->option->pos = it->option->get(menu->data);
493 }
494 }
495
496 menu->anim.movie = resetMovie(&menu->anim.start);
497}
498
499bool studio_menu_back(Menu* menu)
500{
501 if(menu->back)
502 {
503 playSystemSfx(menu->studio, 2);
504 menu->anim.movie = resetMovie(&menu->anim.back);
505 }
506
507 return menu->back != NULL;
508}
509
510void studio_menu_free(Menu* menu)
511{
512 ANIM_STATES(ANIM_FREE);
513
514 FREE(menu->items);
515 free(menu);
516}
517