1 /* RetroArch - A frontend for libretro.
2 * Copyright (C) 2014-2017 - Jean-André Santoni
3 * Copyright (C) 2015-2018 - Andre Leiradella
4 * Copyright (C) 2018-2020 - natinusala
5 *
6 * RetroArch is free software: you can redistribute it and/or modify it under the terms
7 * of the GNU General Public License as published by the Free Software Found-
8 * ation, either version 3 of the License, or (at your option) any later version.
9 *
10 * RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
11 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
12 * PURPOSE. See the GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License along with RetroArch.
15 * If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "../gfx_widgets.h"
19 #include "../gfx_animation.h"
20 #include "../gfx_display.h"
21 #include "../../retroarch.h"
22
23 #define GENERIC_MESSAGE_FADE_DURATION MSG_QUEUE_ANIMATION_DURATION
24
25 /* Widget state */
26
27 enum gfx_widget_generic_message_status
28 {
29 GFX_WIDGET_GENERIC_MESSAGE_IDLE = 0,
30 GFX_WIDGET_GENERIC_MESSAGE_SLIDE_IN,
31 GFX_WIDGET_GENERIC_MESSAGE_FADE_IN,
32 GFX_WIDGET_GENERIC_MESSAGE_WAIT,
33 GFX_WIDGET_GENERIC_MESSAGE_FADE_OUT
34 };
35
36 struct gfx_widget_generic_message_state
37 {
38 unsigned bg_min_width;
39 unsigned bg_width;
40 unsigned bg_height;
41 unsigned frame_width;
42 unsigned text_padding;
43 unsigned text_color;
44
45 unsigned message_duration;
46
47 gfx_timer_t timer; /* float alignment */
48
49 float bg_x;
50 float bg_y_start;
51 float bg_y_end;
52 float text_x;
53 float text_y_start;
54 float text_y_end;
55
56 float alpha;
57
58 float bg_color[16];
59 float frame_color[16];
60
61 enum gfx_widget_generic_message_status status;
62
63 char message[512];
64 bool message_updated;
65 };
66
67 typedef struct gfx_widget_generic_message_state gfx_widget_generic_message_state_t;
68
69 static gfx_widget_generic_message_state_t p_w_generic_message_st = {
70
71 0, /* bg_min_width */
72 0, /* bg_width */
73 0, /* bg_height */
74 0, /* frame_width */
75 0, /* text_padding */
76 0xFFFFFFFF, /* text_color */
77
78 0, /* message_duration */
79
80 0.0f, /* timer */
81
82 0.0f, /* bg_x */
83 0.0f, /* bg_y_start */
84 0.0f, /* bg_y_end */
85 0.0f, /* text_x */
86 0.0f, /* text_y_start */
87 0.0f, /* text_y_end */
88
89 0.0f, /* alpha */
90
91 COLOR_HEX_TO_FLOAT(0x3A3A3A, 1.0f), /* bg_color */
92 COLOR_HEX_TO_FLOAT(0x7A7A7A, 1.0f), /* frame_color */
93
94 GFX_WIDGET_GENERIC_MESSAGE_IDLE, /* status */
95
96 {'\0'}, /* message */
97 false /* message_updated */
98 };
99
100 /* Utilities */
101
gfx_widget_generic_message_reset(bool cancel_pending)102 static void gfx_widget_generic_message_reset(bool cancel_pending)
103 {
104 gfx_widget_generic_message_state_t *state = &p_w_generic_message_st;
105 uintptr_t alpha_tag = (uintptr_t)&state->alpha;
106 uintptr_t timer_tag = (uintptr_t)&state->timer;
107
108 /* Kill any existing timers/animations */
109 gfx_animation_kill_by_tag(&timer_tag);
110 gfx_animation_kill_by_tag(&alpha_tag);
111
112 /* Reset status */
113 state->status = GFX_WIDGET_GENERIC_MESSAGE_IDLE;
114 if (cancel_pending)
115 state->message_updated = false;
116 }
117
118 /* Callbacks */
119
gfx_widget_generic_message_fade_out_cb(void * userdata)120 static void gfx_widget_generic_message_fade_out_cb(void *userdata)
121 {
122 gfx_widget_generic_message_reset(false);
123 }
124
gfx_widget_generic_message_wait_cb(void * userdata)125 static void gfx_widget_generic_message_wait_cb(void *userdata)
126 {
127 gfx_widget_generic_message_state_t *state = (gfx_widget_generic_message_state_t*)userdata;
128 uintptr_t alpha_tag = (uintptr_t)&state->alpha;
129 gfx_animation_ctx_entry_t animation_entry;
130
131 /* Trigger fade out */
132 state->alpha = 1.0f;
133 animation_entry.easing_enum = EASING_OUT_QUAD;
134 animation_entry.tag = alpha_tag;
135 animation_entry.duration = GENERIC_MESSAGE_FADE_DURATION;
136 animation_entry.target_value = 0.0f;
137 animation_entry.subject = &state->alpha;
138 animation_entry.cb = gfx_widget_generic_message_fade_out_cb;
139 animation_entry.userdata = NULL;
140
141 gfx_animation_push(&animation_entry);
142 state->status = GFX_WIDGET_GENERIC_MESSAGE_FADE_OUT;
143 }
144
gfx_widget_generic_message_slide_in_cb(void * userdata)145 static void gfx_widget_generic_message_slide_in_cb(void *userdata)
146 {
147 gfx_widget_generic_message_state_t *state = (gfx_widget_generic_message_state_t*)userdata;
148 gfx_timer_ctx_entry_t timer;
149
150 /* Start wait timer */
151 state->alpha = 1.0f;
152 timer.duration = state->message_duration;
153 timer.cb = gfx_widget_generic_message_wait_cb;
154 timer.userdata = state;
155
156 gfx_animation_timer_start(&state->timer, &timer);
157 state->status = GFX_WIDGET_GENERIC_MESSAGE_WAIT;
158 }
159
160 /* Widget interface */
161
gfx_widget_set_generic_message(void * data,const char * msg,unsigned duration)162 void gfx_widget_set_generic_message(void *data,
163 const char *msg, unsigned duration)
164 {
165 dispgfx_widget_t *p_dispwidget = (dispgfx_widget_t*)data;
166 gfx_widget_generic_message_state_t *state = &p_w_generic_message_st;
167 unsigned last_video_width = p_dispwidget->last_video_width;
168 int text_width = 0;
169 gfx_widget_font_data_t *font_msg_queue = &p_dispwidget->gfx_widget_fonts.msg_queue;
170
171 /* Ensure we have a valid message string */
172 if (string_is_empty(msg))
173 return;
174
175 /* Cache message parameters */
176 strlcpy(state->message, msg, sizeof(state->message));
177 state->message_duration = duration;
178
179 /* Get background width */
180 text_width = font_driver_get_message_width(
181 font_msg_queue->font, state->message,
182 (unsigned)strlen(state->message), 1.0f);
183 if (text_width < 0)
184 text_width = 0;
185 state->bg_width = (state->text_padding * 2) + (unsigned)text_width;
186
187 if (state->bg_width < state->bg_min_width)
188 state->bg_width = state->bg_min_width;
189 if (state->bg_width > last_video_width)
190 state->bg_width = last_video_width;
191
192 /* Update x draw positions */
193 state->bg_x = ((float)last_video_width
194 - (float)state->bg_width) * 0.5f;
195 state->text_x = ((float)last_video_width
196 - (float)text_width) * 0.5f;
197 if (state->text_x < (float)state->text_padding)
198 state->text_x = (float)state->text_padding;
199
200 /* If a 'slide in' animation is already in
201 * progress, no further action is required;
202 * just let animation continue with the updated
203 * message text */
204 if (state->status == GFX_WIDGET_GENERIC_MESSAGE_SLIDE_IN)
205 return;
206
207 /* Signal that message has been updated
208 * > Note that we have to defer the triggering
209 * of any animation changes until the next
210 * call of gfx_widget_generic_message_iterate().
211 * This is because cores will often send messages
212 * during initialisation - i.e. during processes
213 * that take a non-trivial amount of time. In these
214 * cases, updating the animation state here would
215 * result in the following:
216 * - Core starts initialisation/load content
217 * - Message is set, animation is triggered
218 * - Core finishes initialisation/load content,
219 * taking multiple 100's of ms
220 * - On next runloop iterate, animation status
221 * is checked - but because initialisation
222 * took so long, the animation duration has
223 * already elapsed
224 * - Animation 'finishes' immediately, and the
225 * user never sees it... */
226 state->message_updated = true;
227 }
228
229 /* Widget layout() */
230
gfx_widget_generic_message_layout(void * data,bool is_threaded,const char * dir_assets,char * font_path)231 static void gfx_widget_generic_message_layout(
232 void *data,
233 bool is_threaded, const char *dir_assets, char *font_path)
234 {
235 dispgfx_widget_t *p_dispwidget = (dispgfx_widget_t*)data;
236 gfx_widget_generic_message_state_t *state = &p_w_generic_message_st;
237
238 unsigned last_video_width = p_dispwidget->last_video_width;
239 unsigned last_video_height = p_dispwidget->last_video_height;
240 unsigned divider_width = p_dispwidget->divider_width_1px;
241 unsigned widget_padding = 0;
242 int text_width = 0;
243 const float base_aspect = 4.0f / 3.0f;
244 float widget_margin = 0.0f;
245 gfx_widget_font_data_t *font_msg_queue = &p_dispwidget->gfx_widget_fonts.msg_queue;
246
247 /* Set padding values */
248 state->text_padding = (unsigned)(((float)font_msg_queue->line_height * (2.0f / 3.0f)) + 0.5f);
249 widget_padding = state->text_padding * 2;
250
251 /* Set frame width */
252 state->frame_width = divider_width * 2;
253
254 /* Set minimum background width
255 * > Widget nominally occupies the full width
256 * of a 4:3 region at the centre of the screen,
257 * minus padding (it may expand if the text is
258 * too long to fit within these bounds) */
259 widget_margin = (float)last_video_width -
260 (base_aspect * (float)last_video_height);
261 if (widget_margin < 0.0f)
262 widget_margin = 0.0f;
263
264 state->bg_min_width = last_video_width - (unsigned)widget_margin -
265 ((widget_padding + state->frame_width) * 2);
266 if (state->bg_min_width > last_video_width)
267 state->bg_min_width = last_video_width;
268
269 /* Set background width */
270 state->bg_width = state->text_padding * 2;
271
272 if (!string_is_empty(state->message))
273 {
274 text_width = font_driver_get_message_width(
275 font_msg_queue->font, state->message,
276 (unsigned)strlen(state->message), 1.0f);
277 if (text_width < 0)
278 text_width = 0;
279
280 state->bg_width += (unsigned)text_width;
281 }
282
283 if (state->bg_width < state->bg_min_width)
284 state->bg_width = state->bg_min_width;
285 if (state->bg_width > last_video_width)
286 state->bg_width = last_video_width;
287
288 /* Set background height */
289 state->bg_height = font_msg_queue->line_height * 2;
290
291 /* Set background draw positions */
292 state->bg_x = ((float)last_video_width - (float)state->bg_width) * 0.5f;
293 state->bg_y_start = (float)last_video_height + (float)state->frame_width;
294 state->bg_y_end = (float)last_video_height - (float)state->bg_height;
295
296 /* Set text draw positions
297 * > Text is drawn at the horizontal centre
298 * of the screen - unless it is too long to
299 * fit within the bounds of the window, in
300 * which case we shift it to the right such
301 * that the start of the string can be seen */
302 state->text_x = ((float)last_video_width
303 - (float)text_width) * 0.5f;
304 if (state->text_x < (float)state->text_padding)
305 state->text_x = (float)state->text_padding;
306 state->text_y_start = state->bg_y_start +
307 ((float)state->bg_height * 0.5f) +
308 (float)font_msg_queue->line_centre_offset;
309 state->text_y_end = state->bg_y_end +
310 ((float)state->bg_height * 0.5f) +
311 (float)font_msg_queue->line_centre_offset;
312 }
313
314 /* Widget iterate() */
315
gfx_widget_generic_message_iterate(void * user_data,unsigned width,unsigned height,bool fullscreen,const char * dir_assets,char * font_path,bool is_threaded)316 static void gfx_widget_generic_message_iterate(void *user_data,
317 unsigned width, unsigned height, bool fullscreen,
318 const char *dir_assets, char *font_path,
319 bool is_threaded)
320 {
321 gfx_widget_generic_message_state_t *state = &p_w_generic_message_st;
322
323 if (state->message_updated)
324 {
325 enum gfx_widget_generic_message_status current_status = state->status;
326 uintptr_t alpha_tag = (uintptr_t)&state->alpha;
327 gfx_animation_ctx_entry_t animation_entry;
328
329 /* In all cases, reset any existing animation */
330 gfx_widget_generic_message_reset(false);
331
332 /* If an animation was already in progress,
333 * have to continue from the last active
334 * animation phase */
335 switch (current_status)
336 {
337 case GFX_WIDGET_GENERIC_MESSAGE_IDLE:
338 /* Trigger 'slide in' animation */
339 state->alpha = 0.0f;
340 animation_entry.easing_enum = EASING_OUT_QUAD;
341 animation_entry.tag = alpha_tag;
342 animation_entry.duration = GENERIC_MESSAGE_FADE_DURATION;
343 animation_entry.target_value = 1.0f;
344 animation_entry.subject = &state->alpha;
345 animation_entry.cb = gfx_widget_generic_message_slide_in_cb;
346 animation_entry.userdata = state;
347
348 gfx_animation_push(&animation_entry);
349 state->status = GFX_WIDGET_GENERIC_MESSAGE_SLIDE_IN;
350 break;
351 case GFX_WIDGET_GENERIC_MESSAGE_FADE_IN:
352 case GFX_WIDGET_GENERIC_MESSAGE_FADE_OUT:
353 {
354 /* If we are fading in or out, start
355 * a new fade in animation (transitioning
356 * from the current alpha value to 1.0) */
357 unsigned fade_duration = (unsigned)(((1.0f - state->alpha) *
358 (float)GENERIC_MESSAGE_FADE_DURATION) + 0.5f);
359 if (fade_duration > GENERIC_MESSAGE_FADE_DURATION)
360 fade_duration = GENERIC_MESSAGE_FADE_DURATION;
361
362 /* > If current and final alpha values are the
363 * same, or fade duration is zero, skip
364 * straight to the wait phase */
365 if ((state->alpha >= 1.0f) || (fade_duration < 1))
366 gfx_widget_generic_message_slide_in_cb(state);
367 else
368 {
369 animation_entry.easing_enum = EASING_OUT_QUAD;
370 animation_entry.tag = alpha_tag;
371 animation_entry.duration = fade_duration;
372 animation_entry.target_value = 1.0f;
373 animation_entry.subject = &state->alpha;
374 /* Note that 'slide in' and 'fade in' share
375 * the same callback */
376 animation_entry.cb = gfx_widget_generic_message_slide_in_cb;
377 animation_entry.userdata = state;
378
379 gfx_animation_push(&animation_entry);
380 state->status = GFX_WIDGET_GENERIC_MESSAGE_FADE_IN;
381 }
382 }
383 break;
384 case GFX_WIDGET_GENERIC_MESSAGE_WAIT:
385 /* If we are currently waiting, just
386 * 'reset' the wait timer */
387 gfx_widget_generic_message_slide_in_cb(state);
388 break;
389 default:
390 /* The only remaining case is
391 * GFX_WIDGET_GENERIC_MESSAGE_SLIDE_IN,
392 * which can never happen (state->message_updated
393 * will be false if it is, so this code will
394 * not be invoked). If we reach this point, an
395 * unknown error has occurred. We have already
396 * reset any existing animation, so no further
397 * action is required */
398 break;
399 }
400
401 state->message_updated = false;
402 }
403 }
404
405 /* Widget frame() */
406
gfx_widget_generic_message_frame(void * data,void * user_data)407 static void gfx_widget_generic_message_frame(void *data, void *user_data)
408 {
409 gfx_widget_generic_message_state_t *state = &p_w_generic_message_st;
410
411 if (state->status != GFX_WIDGET_GENERIC_MESSAGE_IDLE)
412 {
413 unsigned text_color;
414 float widget_alpha, bg_y, text_y;
415 video_frame_info_t *video_info = (video_frame_info_t*)data;
416 dispgfx_widget_t *p_dispwidget = (dispgfx_widget_t*)user_data;
417
418 unsigned video_width = video_info->width;
419 unsigned video_height = video_info->height;
420 void *userdata = video_info->userdata;
421 gfx_display_t *p_disp = (gfx_display_t*)video_info->disp_userdata;
422 gfx_widget_font_data_t *font_msg_queue = &p_dispwidget->gfx_widget_fonts.msg_queue;
423 size_t msg_queue_size = p_dispwidget->current_msgs_size;
424
425 /* Determine status-dependent opacity/position
426 * values */
427 switch (state->status)
428 {
429 case GFX_WIDGET_GENERIC_MESSAGE_SLIDE_IN:
430 widget_alpha = state->alpha;
431 /* Use 'alpha' to determine the draw offset
432 * > Saves having to trigger two animations */
433 bg_y = state->bg_y_start + (state->alpha *
434 (state->bg_y_end - state->bg_y_start));
435 text_y = state->text_y_start + (state->alpha *
436 (state->text_y_end - state->text_y_start));
437 break;
438 case GFX_WIDGET_GENERIC_MESSAGE_FADE_IN:
439 case GFX_WIDGET_GENERIC_MESSAGE_FADE_OUT:
440 widget_alpha = state->alpha;
441 bg_y = state->bg_y_end;
442 text_y = state->text_y_end;
443 break;
444 case GFX_WIDGET_GENERIC_MESSAGE_WAIT:
445 widget_alpha = 1.0f;
446 bg_y = state->bg_y_end;
447 text_y = state->text_y_end;
448 break;
449 default:
450 widget_alpha = 0.0f;
451 bg_y = state->bg_y_start;
452 text_y = state->text_y_start;
453 break;
454 }
455
456 /* Draw widget */
457 if (widget_alpha > 0.0f)
458 {
459 /* Set opacity */
460 gfx_display_set_alpha(state->bg_color, widget_alpha);
461 gfx_display_set_alpha(state->frame_color, widget_alpha);
462 text_color = COLOR_TEXT_ALPHA(state->text_color,
463 (unsigned)(widget_alpha * 255.0f));
464
465 /* Background */
466 gfx_display_draw_quad(
467 p_disp,
468 userdata,
469 video_width,
470 video_height,
471 state->bg_x,
472 bg_y,
473 state->bg_width,
474 state->bg_height,
475 video_width,
476 video_height,
477 state->bg_color);
478
479 /* Frame */
480
481 /* > Top */
482 gfx_display_draw_quad(
483 p_disp,
484 userdata,
485 video_width,
486 video_height,
487 state->bg_x - (float)state->frame_width,
488 bg_y - (float)state->frame_width,
489 state->bg_width + (state->frame_width * 2),
490 state->frame_width,
491 video_width,
492 video_height,
493 state->frame_color);
494
495 /* > Left */
496 gfx_display_draw_quad(
497 p_disp,
498 userdata,
499 video_width,
500 video_height,
501 state->bg_x - (float)state->frame_width,
502 bg_y,
503 state->frame_width,
504 state->bg_height,
505 video_width,
506 video_height,
507 state->frame_color);
508
509 /* > Right */
510 gfx_display_draw_quad(
511 p_disp,
512 userdata,
513 video_width,
514 video_height,
515 state->bg_x + (float)state->bg_width,
516 bg_y,
517 state->frame_width,
518 state->bg_height,
519 video_width,
520 video_height,
521 state->frame_color);
522
523 /* Message */
524 gfx_widgets_draw_text(
525 font_msg_queue,
526 state->message,
527 state->text_x,
528 text_y,
529 video_width,
530 video_height,
531 text_color,
532 TEXT_ALIGN_LEFT,
533 true);
534
535 /* If the message queue is active, must flush the
536 * text here to avoid overlaps */
537 if (msg_queue_size > 0)
538 gfx_widgets_flush_text(video_width, video_height,
539 font_msg_queue);
540 }
541 }
542 }
543
544 /* Widget free() */
545
gfx_widget_generic_message_free(void)546 static void gfx_widget_generic_message_free(void)
547 {
548 gfx_widget_generic_message_reset(true);
549 }
550
551 /* Widget definition */
552
553 const gfx_widget_t gfx_widget_generic_message = {
554 NULL, /* init */
555 gfx_widget_generic_message_free,
556 NULL, /* context_reset*/
557 NULL, /* context_destroy */
558 gfx_widget_generic_message_layout,
559 gfx_widget_generic_message_iterate,
560 gfx_widget_generic_message_frame
561 };
562