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