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 LIBRETRO_MESSAGE_FADE_DURATION MSG_QUEUE_ANIMATION_DURATION
24 
25 /* Widget state */
26 
27 enum gfx_widget_libretro_message_status
28 {
29    GFX_WIDGET_LIBRETRO_MESSAGE_IDLE = 0,
30    GFX_WIDGET_LIBRETRO_MESSAGE_SLIDE_IN,
31    GFX_WIDGET_LIBRETRO_MESSAGE_FADE_IN,
32    GFX_WIDGET_LIBRETRO_MESSAGE_WAIT,
33    GFX_WIDGET_LIBRETRO_MESSAGE_FADE_OUT
34 };
35 
36 struct gfx_widget_libretro_message_state
37 {
38    unsigned bg_width;
39    unsigned bg_height;
40    unsigned text_padding;
41    unsigned text_color;
42    unsigned frame_width;
43 
44    unsigned message_duration;
45 
46    gfx_timer_t timer;   /* float alignment */
47 
48    float bg_x;
49    float bg_y_start;
50    float bg_y_end;
51    float text_x;
52    float text_y_start;
53    float text_y_end;
54    float alpha;
55 
56    float frame_color[16];
57 
58    enum gfx_widget_libretro_message_status status;
59 
60    char message[512];
61    bool message_updated;
62 };
63 
64 typedef struct gfx_widget_libretro_message_state gfx_widget_libretro_message_state_t;
65 
66 static gfx_widget_libretro_message_state_t p_w_libretro_message_st = {
67 
68    0,                                  /* bg_width */
69    0,                                  /* bg_height */
70    0,                                  /* text_padding */
71    0xE0E0E0FF,                         /* text_color */
72    0,                                  /* frame_width */
73 
74    0,                                  /* message_duration */
75 
76    0.0f,                               /* timer */
77 
78    0.0f,                               /* bg_x */
79    0.0f,                               /* bg_y_start */
80    0.0f,                               /* bg_y_end */
81    0.0f,                               /* text_x */
82    0.0f,                               /* text_y_start */
83    0.0f,                               /* text_y_end */
84    0.0f,                               /* alpha */
85 
86    COLOR_HEX_TO_FLOAT(0x909090, 1.0f), /* frame_color */
87 
88    GFX_WIDGET_LIBRETRO_MESSAGE_IDLE,   /* status */
89 
90    {'\0'},                             /* message */
91    false                               /* message_updated */
92 };
93 
94 /* Utilities */
95 
gfx_widget_libretro_message_reset(bool cancel_pending)96 static void gfx_widget_libretro_message_reset(bool cancel_pending)
97 {
98    gfx_widget_libretro_message_state_t *state = &p_w_libretro_message_st;
99    uintptr_t alpha_tag                        = (uintptr_t)&state->alpha;
100    uintptr_t timer_tag                        = (uintptr_t)&state->timer;
101 
102    /* Kill any existing timers/animations */
103    gfx_animation_kill_by_tag(&timer_tag);
104    gfx_animation_kill_by_tag(&alpha_tag);
105 
106    /* Reset status */
107    state->status             = GFX_WIDGET_LIBRETRO_MESSAGE_IDLE;
108    if (cancel_pending)
109       state->message_updated = false;
110 }
111 
112 /* Callbacks */
113 
gfx_widget_libretro_message_fade_out_cb(void * userdata)114 static void gfx_widget_libretro_message_fade_out_cb(void *userdata)
115 {
116    gfx_widget_libretro_message_reset(false);
117 }
118 
gfx_widget_libretro_message_wait_cb(void * userdata)119 static void gfx_widget_libretro_message_wait_cb(void *userdata)
120 {
121    gfx_widget_libretro_message_state_t *state = (gfx_widget_libretro_message_state_t*)userdata;
122    uintptr_t alpha_tag                        = (uintptr_t)&state->alpha;
123    gfx_animation_ctx_entry_t animation_entry;
124 
125    /* Trigger fade out */
126    state->alpha                 = 1.0f;
127    animation_entry.easing_enum  = EASING_OUT_QUAD;
128    animation_entry.tag          = alpha_tag;
129    animation_entry.duration     = LIBRETRO_MESSAGE_FADE_DURATION;
130    animation_entry.target_value = 0.0f;
131    animation_entry.subject      = &state->alpha;
132    animation_entry.cb           = gfx_widget_libretro_message_fade_out_cb;
133    animation_entry.userdata     = NULL;
134 
135    gfx_animation_push(&animation_entry);
136    state->status = GFX_WIDGET_LIBRETRO_MESSAGE_FADE_OUT;
137 }
138 
gfx_widget_libretro_message_slide_in_cb(void * userdata)139 static void gfx_widget_libretro_message_slide_in_cb(void *userdata)
140 {
141    gfx_widget_libretro_message_state_t *state = (gfx_widget_libretro_message_state_t*)userdata;
142    gfx_timer_ctx_entry_t timer;
143 
144    /* Start wait timer */
145    state->alpha   = 1.0f;
146    timer.duration = state->message_duration;
147    timer.cb       = gfx_widget_libretro_message_wait_cb;
148    timer.userdata = state;
149 
150    gfx_animation_timer_start(&state->timer, &timer);
151    state->status = GFX_WIDGET_LIBRETRO_MESSAGE_WAIT;
152 }
153 
154 /* Widget interface */
155 
gfx_widget_set_libretro_message(void * data,const char * msg,unsigned duration)156 void gfx_widget_set_libretro_message(void *data,
157       const char *msg, unsigned duration)
158 {
159    dispgfx_widget_t *p_dispwidget             = (dispgfx_widget_t*)data;
160    gfx_widget_libretro_message_state_t *state = &p_w_libretro_message_st;
161    gfx_widget_font_data_t *font_msg_queue     = &p_dispwidget->gfx_widget_fonts.msg_queue;
162 
163    /* Ensure we have a valid message string */
164    if (string_is_empty(msg))
165       return;
166 
167    /* Cache message parameters */
168    strlcpy(state->message, msg, sizeof(state->message));
169    state->message_duration = duration;
170 
171    /* Get background width */
172    state->bg_width = (state->text_padding * 2) +
173          font_driver_get_message_width(
174                font_msg_queue->font, state->message,
175                (unsigned)strlen(state->message), 1.0f);
176 
177    /* If a 'slide in' animation is already in
178     * progress, no further action is required;
179     * just let animation continue with the updated
180     * message text */
181    if (state->status == GFX_WIDGET_LIBRETRO_MESSAGE_SLIDE_IN)
182       return;
183 
184    /* Signal that message has been updated
185     * > Note that we have to defer the triggering
186     *   of any animation changes until the next
187     *   call of gfx_widget_libretro_message_iterate().
188     *   This is because cores will often send messages
189     *   during initialisation - i.e. during processes
190     *   that take a non-trivial amount of time. In these
191     *   cases, updating the animation state here would
192     *   result in the following:
193     *   - Core starts initialisation/load content
194     *   - Message is set, animation is triggered
195     *   - Core finishes initialisation/load content,
196     *     taking multiple 100's of ms
197     *   - On next runloop iterate, animation status
198     *     is checked - but because initialisation
199     *     took so long, the animation duration has
200     *     already elapsed
201     *   - Animation 'finishes' immediately, and the
202     *     user never sees it... */
203    state->message_updated = true;
204 }
205 
206 /* Widget layout() */
207 
gfx_widget_libretro_message_layout(void * data,bool is_threaded,const char * dir_assets,char * font_path)208 static void gfx_widget_libretro_message_layout(
209       void *data,
210       bool is_threaded, const char *dir_assets, char *font_path)
211 {
212    dispgfx_widget_t *p_dispwidget             = (dispgfx_widget_t*)data;
213    gfx_widget_libretro_message_state_t *state = &p_w_libretro_message_st;
214 
215    unsigned last_video_height                 = p_dispwidget->last_video_height;
216    unsigned divider_width                     = p_dispwidget->divider_width_1px;
217    gfx_widget_font_data_t *font_msg_queue     = &p_dispwidget->gfx_widget_fonts.msg_queue;
218 
219    /* Set values that are independent of message length */
220    state->bg_height    = font_msg_queue->line_height * 2;
221    state->text_padding = (unsigned)(((float)font_msg_queue->line_height * (2.0f / 3.0f)) + 0.5f);
222    state->frame_width  = divider_width;
223 
224    state->bg_x         = 0.0f;
225    state->bg_y_start   = (float)last_video_height + (float)state->frame_width;
226    state->bg_y_end     = (float)last_video_height - (float)state->bg_height;
227    state->text_x       = (float)state->text_padding;
228    state->text_y_start = state->bg_y_start + ((float)state->bg_height * 0.5f) +
229          (float)font_msg_queue->line_centre_offset;
230    state->text_y_end   = state->bg_y_end + ((float)state->bg_height * 0.5f) +
231          (float)font_msg_queue->line_centre_offset;
232 
233    /* Update values that are dependent upon message length */
234    state->bg_width     = state->text_padding * 2;
235 
236    if (!string_is_empty(state->message))
237       state->bg_width += font_driver_get_message_width(
238             font_msg_queue->font, state->message,
239             (unsigned)strlen(state->message), 1.0f);
240 }
241 
242 /* Widget iterate() */
243 
gfx_widget_libretro_message_iterate(void * user_data,unsigned width,unsigned height,bool fullscreen,const char * dir_assets,char * font_path,bool is_threaded)244 static void gfx_widget_libretro_message_iterate(void *user_data,
245       unsigned width, unsigned height, bool fullscreen,
246       const char *dir_assets, char *font_path,
247       bool is_threaded)
248 {
249    gfx_widget_libretro_message_state_t *state = &p_w_libretro_message_st;
250 
251    if (state->message_updated)
252    {
253       enum gfx_widget_libretro_message_status current_status = state->status;
254       uintptr_t alpha_tag                                    = (uintptr_t)&state->alpha;
255       gfx_animation_ctx_entry_t animation_entry;
256 
257       /* In all cases, reset any existing animation */
258       gfx_widget_libretro_message_reset(false);
259 
260       /* If an animation was already in progress,
261        * have to continue from the last active
262        * animation phase */
263       switch (current_status)
264       {
265          case GFX_WIDGET_LIBRETRO_MESSAGE_IDLE:
266             /* Trigger 'slide in' animation */
267             state->alpha                 = 0.0f;
268             animation_entry.easing_enum  = EASING_OUT_QUAD;
269             animation_entry.tag          = alpha_tag;
270             animation_entry.duration     = LIBRETRO_MESSAGE_FADE_DURATION;
271             animation_entry.target_value = 1.0f;
272             animation_entry.subject      = &state->alpha;
273             animation_entry.cb           = gfx_widget_libretro_message_slide_in_cb;
274             animation_entry.userdata     = state;
275 
276             gfx_animation_push(&animation_entry);
277             state->status = GFX_WIDGET_LIBRETRO_MESSAGE_SLIDE_IN;
278             break;
279          case GFX_WIDGET_LIBRETRO_MESSAGE_FADE_IN:
280          case GFX_WIDGET_LIBRETRO_MESSAGE_FADE_OUT:
281             {
282                /* If we are fading in or out, start
283                 * a new fade in animation (transitioning
284                 * from the current alpha value to 1.0) */
285                unsigned fade_duration = (unsigned)(((1.0f - state->alpha) *
286                      (float)LIBRETRO_MESSAGE_FADE_DURATION) + 0.5f);
287                if (fade_duration > LIBRETRO_MESSAGE_FADE_DURATION)
288                   fade_duration       = LIBRETRO_MESSAGE_FADE_DURATION;
289 
290                /* > If current and final alpha values are the
291                 *   same, or fade duration is zero, skip
292                 *   straight to the wait phase */
293                if ((state->alpha >= 1.0f) || (fade_duration < 1))
294                   gfx_widget_libretro_message_slide_in_cb(state);
295                else
296                {
297                   animation_entry.easing_enum  = EASING_OUT_QUAD;
298                   animation_entry.tag          = alpha_tag;
299                   animation_entry.duration     = fade_duration;
300                   animation_entry.target_value = 1.0f;
301                   animation_entry.subject      = &state->alpha;
302                   /* Note that 'slide in' and 'fade in' share
303                    * the same callback */
304                   animation_entry.cb           = gfx_widget_libretro_message_slide_in_cb;
305                   animation_entry.userdata     = state;
306 
307                   gfx_animation_push(&animation_entry);
308                   state->status = GFX_WIDGET_LIBRETRO_MESSAGE_FADE_IN;
309                }
310             }
311             break;
312          case GFX_WIDGET_LIBRETRO_MESSAGE_WAIT:
313             /* If we are currently waiting, just
314              * 'reset' the wait timer */
315             gfx_widget_libretro_message_slide_in_cb(state);
316             break;
317          default:
318             /* The only remaining case is
319              * GFX_WIDGET_LIBRETRO_MESSAGE_SLIDE_IN,
320              * which can never happen (state->message_updated
321              * will be false if it is, so this code will
322              * not be invoked). If we reach this point, an
323              * unknown error has occurred. We have already
324              * reset any existing animation, so no further
325              * action is required */
326             break;
327       }
328 
329       state->message_updated = false;
330    }
331 }
332 
333 /* Widget frame() */
334 
gfx_widget_libretro_message_frame(void * data,void * user_data)335 static void gfx_widget_libretro_message_frame(void *data, void *user_data)
336 {
337    gfx_widget_libretro_message_state_t *state = &p_w_libretro_message_st;
338 
339    if (state->status != GFX_WIDGET_LIBRETRO_MESSAGE_IDLE)
340    {
341       video_frame_info_t *video_info         = (video_frame_info_t*)data;
342       gfx_display_t *p_disp                  = (gfx_display_t*)video_info->disp_userdata;
343       dispgfx_widget_t *p_dispwidget         = (dispgfx_widget_t*)user_data;
344 
345       unsigned video_width                   = video_info->width;
346       unsigned video_height                  = video_info->height;
347       void *userdata                         = video_info->userdata;
348 
349       gfx_widget_font_data_t *font_msg_queue = &p_dispwidget->gfx_widget_fonts.msg_queue;
350       size_t msg_queue_size                  = p_dispwidget->current_msgs_size;
351       float *bg_color                        = p_dispwidget->backdrop_orig;
352       float bg_alpha;
353       float bg_y;
354 
355       unsigned text_color;
356       float text_alpha;
357       float text_y;
358 
359       /* Determine status-dependent opacity/position
360        * values */
361       switch (state->status)
362       {
363          case GFX_WIDGET_LIBRETRO_MESSAGE_SLIDE_IN:
364             bg_alpha   = DEFAULT_BACKDROP * state->alpha;
365             text_alpha = state->alpha;
366             /* Use 'alpha' to determine the draw offset
367              * > Saves having to trigger two animations */
368             bg_y       = state->bg_y_start + (state->alpha *
369                   (state->bg_y_end - state->bg_y_start));
370             text_y     = state->text_y_start + (state->alpha *
371                   (state->text_y_end - state->text_y_start));
372             break;
373          case GFX_WIDGET_LIBRETRO_MESSAGE_FADE_IN:
374          case GFX_WIDGET_LIBRETRO_MESSAGE_FADE_OUT:
375             bg_alpha   = DEFAULT_BACKDROP * state->alpha;
376             text_alpha = state->alpha;
377             bg_y       = state->bg_y_end;
378             text_y     = state->text_y_end;
379             break;
380          case GFX_WIDGET_LIBRETRO_MESSAGE_WAIT:
381             bg_alpha   = DEFAULT_BACKDROP;
382             text_alpha = 1.0f;
383             bg_y       = state->bg_y_end;
384             text_y     = state->text_y_end;
385             break;
386          default:
387             bg_alpha   = 0.0f;
388             text_alpha = 0.0f;
389             bg_y       = state->bg_y_start;
390             text_y     = state->text_y_start;
391             break;
392       }
393 
394       /* Draw background */
395       if (bg_alpha > 0.0f)
396       {
397          /* Set opacity
398           * > Note: Background and frame have the
399           *   same opacity */
400          gfx_display_set_alpha(bg_color, bg_alpha);
401          gfx_display_set_alpha(state->frame_color, bg_alpha);
402 
403          /* Background */
404          gfx_display_draw_quad(
405                p_disp,
406                userdata,
407                video_width,
408                video_height,
409                state->bg_x,
410                bg_y,
411                state->bg_width,
412                state->bg_height,
413                video_width,
414                video_height,
415                bg_color);
416 
417          /* Frame */
418          gfx_display_draw_quad(
419                p_disp,
420                userdata,
421                video_width,
422                video_height,
423                state->bg_x,
424                bg_y - (float)state->frame_width,
425                state->bg_width + state->frame_width,
426                state->frame_width,
427                video_width,
428                video_height,
429                state->frame_color);
430 
431          gfx_display_draw_quad(
432                p_disp,
433                userdata,
434                video_width,
435                video_height,
436                state->bg_x + (float)state->bg_width,
437                bg_y,
438                state->frame_width,
439                state->bg_height,
440                video_width,
441                video_height,
442                state->frame_color);
443       }
444 
445       /* Draw text */
446       if (text_alpha > 0.0f)
447       {
448          /* Set opacity */
449          text_color = COLOR_TEXT_ALPHA(state->text_color,
450                (unsigned)(text_alpha * 255.0f));
451 
452          /* Draw message */
453          gfx_widgets_draw_text(
454                font_msg_queue,
455                state->message,
456                state->text_x,
457                text_y,
458                video_width,
459                video_height,
460                text_color,
461                TEXT_ALIGN_LEFT,
462                true);
463 
464          /* If the message queue is active, must flush the
465           * text here to avoid overlaps */
466          if (msg_queue_size > 0)
467             gfx_widgets_flush_text(video_width, video_height,
468                   font_msg_queue);
469       }
470    }
471 }
472 
473 /* Widget free() */
474 
gfx_widget_libretro_message_free(void)475 static void gfx_widget_libretro_message_free(void)
476 {
477    gfx_widget_libretro_message_reset(true);
478 }
479 
480 /* Widget definition */
481 
482 const gfx_widget_t gfx_widget_libretro_message = {
483    NULL, /* init */
484    gfx_widget_libretro_message_free,
485    NULL, /* context_reset*/
486    NULL, /* context_destroy */
487    gfx_widget_libretro_message_layout,
488    gfx_widget_libretro_message_iterate,
489    gfx_widget_libretro_message_frame
490 };
491