From daeff97f7eed2caf08206987309ab4107c9f1f66 Mon Sep 17 00:00:00 2001 From: jdgleaver Date: Thu, 29 Aug 2019 16:46:49 +0100 Subject: [PATCH] (XMB) Add smooth scrolling vertical line ticker (for sublabels) --- configuration.c | 1 - menu/drivers/xmb.c | 124 ++++++++++++++-- menu/menu_animation.c | 334 ++++++++++++++++++++++++++++++++++++++---- menu/menu_animation.h | 31 +++- 4 files changed, 442 insertions(+), 48 deletions(-) diff --git a/configuration.c b/configuration.c index 95786ac6c7..93cac5bc18 100644 --- a/configuration.c +++ b/configuration.c @@ -1329,7 +1329,6 @@ static struct config_bool_setting *populate_settings_bool(settings_t *settings, SETTING_BOOL("frame_time_counter_reset_after_load_state", &settings->bools.frame_time_counter_reset_after_load_state, true, false, false); SETTING_BOOL("frame_time_counter_reset_after_save_state", &settings->bools.frame_time_counter_reset_after_save_state, true, false, false); SETTING_BOOL("crt_switch_resolution_use_custom_refresh_rate", &settings->bools.crt_switch_custom_refresh_enable, true, false, false); - SETTING_BOOL("crt_switch_resolution_use_custom_refresh_rate", &settings->bools.crt_switch_custom_refresh_enable, true, false, false); SETTING_BOOL("automatically_add_content_to_playlist", &settings->bools.automatically_add_content_to_playlist, true, DEFAULT_AUTOMATICALLY_ADD_CONTENT_TO_PLAYLIST, false); SETTING_BOOL("ui_companion_start_on_boot", &settings->bools.ui_companion_start_on_boot, true, ui_companion_start_on_boot, false); SETTING_BOOL("ui_companion_enable", &settings->bools.ui_companion_enable, true, ui_companion_enable, false); diff --git a/menu/drivers/xmb.c b/menu/drivers/xmb.c index ed90246758..fcf3612c39 100644 --- a/menu/drivers/xmb.c +++ b/menu/drivers/xmb.c @@ -3037,35 +3037,127 @@ static int xmb_draw_item( if (i == current && width > 320 && height > 240 && !string_is_empty(entry->sublabel)) { - menu_animation_ctx_line_ticker_t line_ticker; char entry_sublabel[MENU_SUBLABEL_MAX_LENGTH]; + menu_animation_ctx_line_ticker_t line_ticker; + menu_animation_ctx_line_ticker_smooth_t line_ticker_smooth = {0}; + unsigned ticker_line_height = 0; + unsigned ticker_num_lines = 0; + float ticker_y_offset = 0.0f; + bool do_scissor = false; + float sublabel_x = 0.0f; + float sublabel_y = 0.0f; entry_sublabel[0] = '\0'; - line_ticker.type_enum = (enum menu_animation_ticker_type)settings->uints.menu_ticker_type; - line_ticker.idx = menu_animation_get_ticker_idx(); + if (use_smooth_ticker) + { + line_ticker_smooth.scissor_enabled = true; + line_ticker_smooth.type_enum = (enum menu_animation_ticker_type)settings->uints.menu_ticker_type; + line_ticker_smooth.idx = menu_animation_get_ticker_pixel_idx(); - line_ticker.line_width = (size_t)(line_ticker_width); - /* Note: max_lines should be calculated at runtime, - * but this is a nuisance. There is room for 4 lines - * to be displayed when using all existing XMB themes, - * so leave this value hard coded for now. */ - line_ticker.max_lines = 4; + line_ticker_smooth.font = xmb->font2; + line_ticker_smooth.font_scale = 1.0f; - line_ticker.s = entry_sublabel; - line_ticker.len = sizeof(entry_sublabel); - line_ticker.str = entry->sublabel; + line_ticker_smooth.field_width = (unsigned)(xmb->font2_size * 0.6f * line_ticker_width); + /* The calculation here is incredibly obtuse. I think + * this is correct... (c.f. xmb_item_y()) */ + line_ticker_smooth.field_height = (unsigned)( + (xmb->icon_spacing_vertical * ((1 + xmb->under_item_offset) - xmb->active_item_factor)) - + (xmb->margins_label_top * 4.0f)); /* Should be 3.5f, but prefer the extra padding */ - menu_animation_line_ticker(&line_ticker); + line_ticker_smooth.src_str = entry->sublabel; + line_ticker_smooth.dst_str = entry_sublabel; + line_ticker_smooth.dst_str_len = sizeof(entry_sublabel); + + line_ticker_smooth.line_height = &ticker_line_height; + line_ticker_smooth.num_lines = &ticker_num_lines; + line_ticker_smooth.y_offset = &ticker_y_offset; + + do_scissor = menu_animation_line_ticker_smooth(&line_ticker_smooth); + } + else + { + line_ticker.type_enum = (enum menu_animation_ticker_type)settings->uints.menu_ticker_type; + line_ticker.idx = menu_animation_get_ticker_idx(); + + line_ticker.line_len = (size_t)(line_ticker_width); + /* Note: max_lines should be calculated at runtime, + * but this is a nuisance. There is room for 4 lines + * to be displayed when using all existing XMB themes, + * so leave this value hard coded for now. */ + line_ticker.max_lines = 4; + + line_ticker.s = entry_sublabel; + line_ticker.len = sizeof(entry_sublabel); + line_ticker.str = entry->sublabel; + + menu_animation_line_ticker(&line_ticker); + } label_offset = - xmb->margins_label_top; + /* Base draw position */ + sublabel_x = node->x + xmb->margins_screen_left + + xmb->icon_spacing_horizontal + xmb->margins_label_left; + sublabel_y = xmb->margins_screen_top + + node->y + (xmb->margins_label_top * 3.5f); + + if (do_scissor) + { + /* We are currently 'blending', so stop */ + menu_display_blend_end(video_info); + /* These font shenanigans seem to be requied before + * calling menu_display_scissor_begin() */ + font_driver_flush(video_info->width, video_info->height, xmb->font2, video_info); + xmb->raster_block2.carr.coords.vertices = 0; + /* TODO/FIXME + * Okay, font handling in RetroArch sucks... + * - It seems that text is drawn relative to the baseline, + * which kinda-sorta makes sense... + * - But there's no way to extract any useful font metrics + * such as descender/ascender height and baseline position + * So basically, when drawing text you pick a y postion and + * hope for the best - the text will appear somewhere near the + * place you want it to, but not quite, and trying to clip text + * to a specified draw area is basically impossible. + * The *correct* way to implement a font library/handler is to + * deal with all font metrics internally such that the y draw + * position is the vertical centre of the line. Since you know + * the line height, this makes all layout operations trivial. + * This should be done at some point, but I don't have time + * to rewrite all of the font handling code for the sake of a + * single line ticker, so for now we'll just make do... + * So here it is: we want to clip the scrolling text such that + * it fills a vertical region eqivalent to the maximum number + * of static sublabel lines that can be shown. Since we don't + * know any font metrics, we have to use a fudge factor for + * the scissor start position (the 0.75 comes from the fact that + * for a typical font, the descender is ~20-30% of the line + * height). *This is not robust*, but it works well enough for + * all existing XMB themes */ + menu_display_scissor_begin( + video_info, (int)sublabel_x, (int)((float)sublabel_y - ((float)ticker_line_height * 0.75f)), + (unsigned)((float)video_info->width - sublabel_x), + ticker_num_lines * ticker_line_height); + } + + /* Only apply ticker y offset when actually + * drawing the text */ xmb_draw_text(video_info, xmb, entry_sublabel, - node->x + xmb->margins_screen_left + - xmb->icon_spacing_horizontal + xmb->margins_label_left, - xmb->margins_screen_top + node->y + xmb->margins_label_top*3.5, + sublabel_x, ticker_y_offset + sublabel_y, 1, node->label_alpha, TEXT_ALIGN_LEFT, width, height, xmb->font2); + + if (do_scissor) + { + /* These font shenanigans seem to be requied before + * calling menu_display_scissor_end() */ + font_driver_flush(video_info->width, video_info->height, xmb->font2, video_info); + xmb->raster_block2.carr.coords.vertices = 0; + menu_display_scissor_end(video_info); + /* Resume 'blending' */ + menu_display_blend_begin(video_info); + } } } diff --git a/menu/menu_animation.c b/menu/menu_animation.c index f9f61db1e1..9ff22f3dcd 100644 --- a/menu/menu_animation.c +++ b/menu/menu_animation.c @@ -779,23 +779,23 @@ static void menu_animation_ticker_smooth_loop(uint64_t idx, } } -static size_t get_line_display_ticks(size_t line_width) +static size_t get_line_display_ticks(size_t line_len) { /* Mean human reading speed for all western languages, * characters per minute */ float cpm = 1000.0f; /* Base time for which a line should be shown, in us */ - float line_duration = (line_width * 60.0f * 1000.0f * 1000.0f) / cpm; + float line_duration = (line_len * 60.0f * 1000.0f * 1000.0f) / cpm; /* Ticker updates (nominally) once every TICKER_SPEED us * > Return base number of ticks for which line should be shown */ return (size_t)(line_duration / (float)TICKER_SPEED); } static void menu_animation_line_ticker_generic(uint64_t idx, - size_t line_width, size_t max_lines, size_t num_lines, + size_t line_len, size_t max_lines, size_t num_lines, size_t *line_offset) { - size_t line_ticks = get_line_display_ticks(line_width); + size_t line_ticks = get_line_display_ticks(line_len); /* Note: This function is only called if num_lines > max_lines */ size_t excess_lines = num_lines - max_lines; /* Ticker will pause for one line duration when the first @@ -822,10 +822,10 @@ static void menu_animation_line_ticker_generic(uint64_t idx, } static void menu_animation_line_ticker_loop(uint64_t idx, - size_t line_width, size_t num_lines, + size_t line_len, size_t num_lines, size_t *line_offset) { - size_t line_ticks = get_line_display_ticks(line_width); + size_t line_ticks = get_line_display_ticks(line_len); size_t ticker_period = num_lines + 1; size_t phase = (idx / line_ticks) % ticker_period; @@ -833,6 +833,121 @@ static void menu_animation_line_ticker_loop(uint64_t idx, *line_offset = phase; } +static size_t get_line_smooth_scroll_ticks(size_t line_len) +{ + /* Mean human reading speed for all western languages, + * characters per minute */ + float cpm = 1000.0f; + /* Base time for which a line should be shown, in ms */ + float line_duration = (line_len * 60.0f * 1000.0f) / cpm; + /* Ticker updates (nominally) once every ticker_pixel_period ms + * > Return base number of ticks for which text should scroll + * from one line to the next */ + return (size_t)(line_duration / ticker_pixel_period); +} + +static void menu_animation_line_ticker_smooth_generic(uint64_t idx, + bool scissor_enabled, size_t line_len, size_t line_height, + size_t max_display_lines, size_t num_lines, + size_t *num_display_lines, size_t *line_offset, float *y_offset) +{ + size_t scroll_ticks = get_line_smooth_scroll_ticks(line_len); + /* Note: This function is only called if num_lines > max_display_lines */ + size_t excess_lines = num_lines - max_display_lines; + /* Ticker will pause for one line duration when the first + * or last line is reached */ + size_t ticker_period = ((excess_lines * 2) + 2) * scroll_ticks; + size_t phase = idx % ticker_period; + size_t line_phase = 0; + bool pause = false; + bool scroll_up = true; + + /* Pause on first line */ + if (phase < scroll_ticks) + pause = true; + phase = (phase >= scroll_ticks) ? phase - scroll_ticks : 0; + /* Pause on last line and change direction */ + if (phase >= excess_lines * scroll_ticks) + { + scroll_up = false; + + if (phase < (excess_lines + 1) * scroll_ticks) + { + pause = true; + phase = 0; + } + else + phase -= (excess_lines + 1) * scroll_ticks; + } + + /* If we are currently paused, can use static offsets */ + if (pause) + { + *num_display_lines = max_display_lines; + *line_offset = scroll_up ? 0 : excess_lines; + *y_offset = 0.0f; + } + else + { + line_phase = phase % scroll_ticks; + + if (scissor_enabled) + { + *num_display_lines = max_display_lines + 1; + + if (scroll_up) + { + *line_offset = phase / scroll_ticks; + *y_offset = (float)line_height * (((float)(scroll_ticks - line_phase) / (float)scroll_ticks) - 1.0f); + } + else + { + *line_offset = (excess_lines - 1) - (phase / scroll_ticks); + *y_offset = (float)line_height * ((1.0f - (float)(scroll_ticks - line_phase) / (float)scroll_ticks) - 1.0f); + } + } + else + { + *num_display_lines = max_display_lines - 1; + + if (scroll_up) + { + *line_offset = (phase / scroll_ticks) + 1; + *y_offset = (float)line_height * (float)(scroll_ticks - line_phase) / (float)scroll_ticks; + } + else + { + *line_offset = excess_lines - (phase / scroll_ticks); + *y_offset = (float)line_height * (1.0f - (float)(scroll_ticks - line_phase) / (float)scroll_ticks); + } + } + } +} + +static void menu_animation_line_ticker_smooth_loop(uint64_t idx, + bool scissor_enabled, size_t line_len, size_t line_height, + size_t max_display_lines, size_t num_lines, + size_t *num_display_lines, size_t *line_offset, float *y_offset) +{ + size_t scroll_ticks = get_line_smooth_scroll_ticks(line_len); + size_t ticker_period = (num_lines + 1) * scroll_ticks; + size_t phase = idx % ticker_period; + size_t line_phase = phase % scroll_ticks; + + *line_offset = phase / scroll_ticks; + + if (scissor_enabled) + { + *num_display_lines = max_display_lines + 1; + *y_offset = (float)line_height * (((float)(scroll_ticks - line_phase) / (float)scroll_ticks) - 1.0f); + } + else + { + *num_display_lines = max_display_lines - 1; + *y_offset = (float)line_height * (float)(scroll_ticks - line_phase) / (float)scroll_ticks; + } +} + static void menu_delayed_animation_cb(void *userdata) { menu_delayed_animation_t *delayed_animation = (menu_delayed_animation_t*) userdata; @@ -1656,9 +1771,31 @@ end: return is_active; } -bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker) +static void build_line_ticker_string( + size_t num_display_lines, size_t line_offset, struct string_list *lines, + char *dest_str, size_t dest_str_len) { size_t i; + + for (i = 0; i < num_display_lines; i++) + { + size_t offset = i + line_offset; + size_t line_index = offset % (lines->size + 1); + bool line_valid = true; + + if (line_index >= lines->size) + line_valid = false; + + if (line_valid) + strlcat(dest_str, lines->elems[line_index].data, dest_str_len); + + if (i < num_display_lines - 1) + strlcat(dest_str, "\n", dest_str_len); + } +} + +bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker) +{ char *wrapped_str = NULL; struct string_list *lines = NULL; size_t line_offset = 0; @@ -1670,7 +1807,7 @@ bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker) return false; if (string_is_empty(line_ticker->str) || - (line_ticker->line_width < 1) || + (line_ticker->line_len < 1) || (line_ticker->max_lines < 1)) goto end; @@ -1682,7 +1819,7 @@ bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker) word_wrap( wrapped_str, line_ticker->str, - (int)line_ticker->line_width, + (int)line_ticker->line_len, true, 0); if (string_is_empty(wrapped_str)) @@ -1709,7 +1846,7 @@ bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker) { menu_animation_line_ticker_loop( line_ticker->idx, - line_ticker->line_width, + line_ticker->line_len, lines->size, &line_offset); @@ -1720,7 +1857,7 @@ bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker) { menu_animation_line_ticker_generic( line_ticker->idx, - line_ticker->line_width, + line_ticker->line_len, line_ticker->max_lines, lines->size, &line_offset); @@ -1730,25 +1867,9 @@ bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker) } /* Build output string from required lines */ - for (i = 0; i < line_ticker->max_lines; i++) - { - size_t offset = i + line_offset; - size_t line_index = 0; - bool line_valid = true; - - if (offset < lines->size) - line_index = offset; - else if (offset > lines->size) - line_index = (offset - 1) - lines->size; - else - line_valid = false; - - if (line_valid) - strlcat(line_ticker->s, lines->elems[line_index].data, line_ticker->len); - - if (i < line_ticker->max_lines - 1) - strlcat(line_ticker->s, "\n", line_ticker->len); - } + build_line_ticker_string( + line_ticker->max_lines, line_offset, lines, + line_ticker->s, line_ticker->len); success = true; is_active = true; @@ -1775,6 +1896,159 @@ end: return is_active; } +bool menu_animation_line_ticker_smooth(menu_animation_ctx_line_ticker_smooth_t *line_ticker) +{ + char *wrapped_str = NULL; + struct string_list *lines = NULL; + int glyph_width = 0; + int glyph_height = 0; + size_t line_len = 0; + size_t max_display_lines = 0; + size_t num_display_lines = 0; + size_t line_offset = 0; + bool success = false; + bool is_active = false; + + /* Sanity check */ + if (!line_ticker) + return false; + + if (!line_ticker->font || + string_is_empty(line_ticker->src_str) || + (line_ticker->field_width < 1) || + (line_ticker->field_height < 1)) + goto end; + + /* Get font dimensions */ + + /* > Width + * This is a bit of a fudge. Performing a 'font aware' + * (i.e. character display width) word wrap is too CPU + * intensive, so we just sample the width of a common + * character and hope for the best. (We choose 'a' because + * this is what Ozone uses for spacing calculations, and + * it is proven to work quite well) */ + glyph_width = font_driver_get_message_width( + line_ticker->font, "a", 1, line_ticker->font_scale); + + if (glyph_width < 0) + goto end; + + /* > Height */ + glyph_height = font_driver_get_line_height( + line_ticker->font, line_ticker->font_scale); + + if (glyph_height < 0) + goto end; + + *line_ticker->line_height = (unsigned)glyph_height; + + /* Determine line wrap parameters */ + line_len = (size_t)(line_ticker->field_width / glyph_width); + max_display_lines = (size_t)(line_ticker->field_height / glyph_height); + + if ((line_len < 1) || (max_display_lines < 1)) + goto end; + + /* Line wrap input string */ + wrapped_str = (char*)malloc((strlen(line_ticker->src_str) + 1) * sizeof(char)); + if (!wrapped_str) + goto end; + + word_wrap( + wrapped_str, + line_ticker->src_str, + (int)line_len, + true, 0); + + if (string_is_empty(wrapped_str)) + goto end; + + /* Split into component lines */ + lines = string_split(wrapped_str, "\n"); + if (!lines) + goto end; + + /* Check whether total number of lines fits within + * the set field limit */ + if (lines->size <= max_display_lines) + { + strlcpy(line_ticker->dst_str, wrapped_str, line_ticker->dst_str_len); + *line_ticker->num_lines = (unsigned)lines->size; + *line_ticker->y_offset = 0.0f; + success = true; + goto end; + } + + *line_ticker->num_lines = (unsigned)max_display_lines; + + /* Determine which lines should be shown, along with + * y axis draw offset */ + switch (line_ticker->type_enum) + { + case TICKER_TYPE_LOOP: + { + menu_animation_line_ticker_smooth_loop( + line_ticker->idx, + line_ticker->scissor_enabled, + line_len, + (size_t)glyph_height, + max_display_lines, + lines->size, + &num_display_lines, + &line_offset, + line_ticker->y_offset); + + break; + } + case TICKER_TYPE_BOUNCE: + default: + { + menu_animation_line_ticker_smooth_generic( + line_ticker->idx, + line_ticker->scissor_enabled, + line_len, + (size_t)glyph_height, + max_display_lines, + lines->size, + &num_display_lines, + &line_offset, + line_ticker->y_offset); + + break; + } + } + + /* Build output string from required lines */ + build_line_ticker_string( + num_display_lines, line_offset, lines, + line_ticker->dst_str, line_ticker->dst_str_len); + + success = true; + is_active = true; + ticker_is_active = true; + +end: + + if (wrapped_str) + { + free(wrapped_str); + wrapped_str = NULL; + } + + if (lines) + { + string_list_free(lines); + lines = NULL; + } + + if (!success) + if (line_ticker->dst_str_len > 0) + line_ticker->dst_str[0] = '\0'; + + return is_active; +} + bool menu_animation_is_active(void) { return animation_is_active || ticker_is_active; diff --git a/menu/menu_animation.h b/menu/menu_animation.h index 47fa7fa025..f3213962b9 100644 --- a/menu/menu_animation.h +++ b/menu/menu_animation.h @@ -147,7 +147,7 @@ typedef struct menu_animation_ctx_ticker_smooth typedef struct menu_animation_ctx_line_ticker { - size_t line_width; + size_t line_len; size_t max_lines; uint64_t idx; enum menu_animation_ticker_type type_enum; @@ -156,6 +156,23 @@ typedef struct menu_animation_ctx_line_ticker const char *str; } menu_animation_ctx_line_ticker_t; +typedef struct menu_animation_ctx_line_ticker_smooth +{ + bool scissor_enabled; + font_data_t *font; + float font_scale; + unsigned field_width; + unsigned field_height; + enum menu_animation_ticker_type type_enum; + uint64_t idx; + const char *src_str; + char *dst_str; + size_t dst_str_len; + unsigned *line_height; + unsigned *num_lines; + float *y_offset; +} menu_animation_ctx_line_ticker_smooth_t; + typedef float menu_timer_t; typedef struct menu_timer_ctx_entry @@ -183,6 +200,18 @@ bool menu_animation_ticker_smooth(menu_animation_ctx_ticker_smooth_t *ticker); bool menu_animation_line_ticker(menu_animation_ctx_line_ticker_t *line_ticker); +/* Note: When line_ticker->scissor_enabled is true, + * resultant string must be drawn in conjunction with + * menu_display_scissor_*() + * i.e. draw area must be scissored vertically by + * (line_ticker->line_height * line_ticker->num_lines), + * with a scissor y start position of text y postion + * (ignoring line_ticker->y_offset) *minus* + * (line_ticker->line_height - font_descender_size). + * font_descender_size is typically 20-30% of the line + * height... */ +bool menu_animation_line_ticker_smooth(menu_animation_ctx_line_ticker_smooth_t *line_ticker); + float menu_animation_get_delta_time(void); bool menu_animation_is_active(void);