Files
RetroArch/menu/drivers/xmb.c
T
LibretroAdmin b9be38fd55 strl: copy string literals without scanning them at runtime
strlcpy() begins with strlen() of its source. It lives in another
translation unit, so with no LTO that scan happens at runtime even when
the source is a literal whose length the compiler already measured, and
the call itself cannot be folded away either.

strlcpy_lit() passes the length over as sizeof(). When the destination
size is also a constant -- sizeof(buf), the usual case -- nothing is
left but the stores memcpy() compiles to:

  strlcpy      "core_options"  sizeof(buf)   7.01 ns
  strlcpy_lit  "core_options"  sizeof(buf)   0.42 ns
  strlcpy      41-char literal sizeof(buf)   7.74 ns
  strlcpy_lit  41-char literal sizeof(buf)   0.71 ns
  strlcpy      "core_options"  runtime len   7.11 ns
  strlcpy_lit  "core_options"  runtime len   0.70 ns

The destination bound is kept. When the literal does not fit it defers
to strlcpy(), so the copy truncates as before rather than overrunning;
a form that took only the literal would have turned every one of these
sites into an overflow waiting for a small buffer. The literal is
concatenated with "" so a non-literal source is a compile error rather
than a sizeof() that silently measures a pointer.

818 sites across 104 files were converted by parsing arguments rather
than by pattern-matching text: only a three-argument call whose second
argument is a bare string literal is rewritten, so a macro that happens
to expand to a literal is left alone. Line endings are preserved, so
the CRLF files carry only their own changed lines.

test/compat/test_strl.c compares the two against each other over every
destination size from 0 past the literal's length, for literals that
are empty, single-character, path-shaped, adjacently concatenated and
escape-bearing, checking the return, the terminator and the whole
buffer so an overrun past the terminator is caught. ASan+UBSan+LSan and
TSan both 4/4. The full menu_displaylist_build_list walk over every
displaylist type is byte-identical to before the change
(md5 a2a081bcac09cdc8d72d67efd14ac40e).
2026-08-30 02:10:20 +02:00

11446 lines
412 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* RetroArch - A frontend for libretro.
* Copyright (C) 2011-2017 - Daniel De Matteis
* Copyright (C) 2014-2017 - Jean-André Santoni
* Copyright (C) 2016-2019 - Brad Parker
* Copyright (C) 2018 - Alfredo Monclús
*
* RetroArch is free software: you can redistribute it and/or modify it under the terms
* of the GNU General Public License as published by the Free Software Found-
* ation, either version 3 of the License, or (at your option) any later version.
*
* RetroArch is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
* PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with RetroArch.
* If not, see <http://www.gnu.org/licenses/>.
*/
#include <stdlib.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <limits.h>
#include <file/file_path.h>
#include <compat/posix_string.h>
#include <compat/strl.h>
#include <formats/image.h>
#include <string/stdstring.h>
#include <gfx/math/matrix_4x4.h>
#include <streams/file_stream.h>
#include <encodings/utf.h>
#include <features/features_cpu.h>
#include <array/rhmap.h>
#include <retro_math.h>
#ifdef HAVE_CONFIG_H
#include "../../config.h"
#endif
#include "../../frontend/frontend_driver.h"
#include "../menu_driver.h"
#include "../menu_str.h"
#include "../menu_entries.h"
#include "../menu_screensaver.h"
#include "../../gfx/gfx_animation.h"
#include "../../gfx/gfx_thumbnail.h"
#include "../../configuration.h"
#include "../../content.h"
#include "../../core_info.h"
#include "../../file_path_special.h"
#include "../../input/input_osk.h"
#include "../../tasks/tasks_internal.h"
#include "../../msg_hash_lbl_str.h"
#ifdef HAVE_AUDIOMIXER
#include "../../audio/audio_driver.h"
#endif
#ifdef HAVE_CHEEVOS
#include "../../cheevos/cheevos_menu.h"
#endif
/* Force a helper out of line even though it has a single call site.
*
* Follows the RXML_NOINLINE precedent in
* libretro-common/formats/xml/rxml.c.
*
* Two reasons it is needed here. An inlined helper's stack slots are
* charged to the caller's frame on every call, whether or not the
* helper's branch is taken, which matters on the per-entry draw path.
* And xmb_frame() is already factored into named phases, but the ones
* called exactly once get inlined straight back into it, so branches
* that are not taken on a given frame still occupy the fall-through
* path in L1i.
*
* Under -Os the compiler already optimises for size and the forced
* outlining only adds call overhead, so it is disabled there. */
#if defined(__OPTIMIZE_SIZE__)
#define XMB_NOINLINE
#elif defined(__GNUC__) && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 3))
#define XMB_NOINLINE __attribute__((noinline))
#elif defined(_MSC_VER)
#define XMB_NOINLINE __declspec(noinline)
#else
#define XMB_NOINLINE
#endif
#define XMB_RIBBON_ROWS (64)
#define XMB_RIBBON_COLS (64)
#define XMB_RIBBON_VERTICES (XMB_RIBBON_ROWS * (2 * XMB_RIBBON_COLS)) - (2 * XMB_RIBBON_COLS)
#define XMB_TAB_MAX_LENGTH 255
#define XMB_DELAY (166.66667f)
#define XMB_EASING_ALPHA EASING_OUT_CIRC
#define XMB_EASING_XY EASING_OUT_QUAD
/* Specifies minimum period (in usec) between
* tab switch events when input repeat is
* active (i.e. when navigating between top level
* menu categories by *holding* left/right on
* RetroPad or keyboard)
* > Note: We want to set a value of 100 ms
* here, but doing so leads to bad pacing when
* running at 60 Hz (due to random frame time
* deviations - input repeat cycles always take
* slightly more or less than 100 ms, so tab
* switches occur every n or (n + 1) frames,
* which gives the appearance of stuttering).
* Reducing the delay by 1 ms accommodates
* any timing fluctuations, resulting in
* smooth motion */
#define XMB_TAB_SWITCH_REPEAT_DELAY (99000)
/* XMB does not have a clean colour theme
* implementation. Until this is available,
* the menu screensaver tint will be set to
* a fixed colour: HTML WhiteSmoke */
#define XMB_SCREENSAVER_TINT 0xF5F5F5
/* Mean human reading speed for all western languages,
* characters per minute */
#define TICKER_CPM (1000.0f)
/* Base time for which a line should be shown, in us */
#define TICKER_LINE_DURATION_US(line_len) ((line_len * 60.0f * 1000.0f * 1000.0f) / TICKER_CPM)
/* Base time for which a line should be shown, in ms */
#define TICKER_LINE_DURATION_MS(line_len) ((line_len * 60.0f * 1000.0f) / TICKER_CPM)
/* Ticker updates (nominally) once every TICKER_SPEED us
* > Base number of ticks for which line should be shown */
#define TICKER_LINE_DISPLAY_TICKS(line_len) ((size_t)(TICKER_LINE_DURATION_US(line_len) / (float)TICKER_SPEED))
/* Smooth ticker updates (nominally) once every TICKER_PIXEL_PERIOD ms
* > Base number of ticks for which text should scroll
* from one line to the next */
#define TICKER_LINE_SMOOTH_SCROLL_TICKS(line_len) ((size_t)(TICKER_LINE_DURATION_MS(line_len) / TICKER_PIXEL_PERIOD))
enum
{
XMB_TEXTURE_MAIN_MENU = 0,
XMB_TEXTURE_SETTINGS,
XMB_TEXTURE_HISTORY,
XMB_TEXTURE_FAVORITES,
#ifdef HAVE_IMAGEVIEWER
XMB_TEXTURE_IMAGES,
#endif
XMB_TEXTURE_MUSICS,
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
XMB_TEXTURE_MOVIES,
#endif
#ifdef HAVE_NETWORKING
XMB_TEXTURE_NETPLAY,
XMB_TEXTURE_ROOM,
XMB_TEXTURE_ROOM_LAN,
XMB_TEXTURE_ROOM_RELAY,
#endif
XMB_TEXTURE_SETTING,
XMB_TEXTURE_SUBSETTING,
XMB_TEXTURE_ARROW,
XMB_TEXTURE_RUN,
XMB_TEXTURE_CLOSE,
XMB_TEXTURE_RESUME,
XMB_TEXTURE_SAVESTATE,
XMB_TEXTURE_LOADSTATE,
XMB_TEXTURE_RECORDREPLAY,
XMB_TEXTURE_PLAYREPLAY,
XMB_TEXTURE_HALTREPLAY,
XMB_TEXTURE_UNDO,
XMB_TEXTURE_CORE_INFO,
XMB_TEXTURE_BLUETOOTH,
XMB_TEXTURE_WIFI,
XMB_TEXTURE_CORE_OPTIONS,
XMB_TEXTURE_INPUT_REMAPPING_OPTIONS,
XMB_TEXTURE_CHEAT_OPTIONS,
XMB_TEXTURE_DISK_OPTIONS,
XMB_TEXTURE_SHADER_OPTIONS,
XMB_TEXTURE_ACHIEVEMENT_LIST,
XMB_TEXTURE_SCREENSHOT,
XMB_TEXTURE_RELOAD,
XMB_TEXTURE_RENAME,
XMB_TEXTURE_FILE,
XMB_TEXTURE_FOLDER,
XMB_TEXTURE_ZIP,
XMB_TEXTURE_FAVORITE,
XMB_TEXTURE_ADD_FAVORITE,
XMB_TEXTURE_IMAGE,
XMB_TEXTURE_MUSIC,
XMB_TEXTURE_MOVIE,
XMB_TEXTURE_CORE,
XMB_TEXTURE_RDB,
XMB_TEXTURE_CURSOR,
XMB_TEXTURE_SWITCH_ON,
XMB_TEXTURE_SWITCH_OFF,
XMB_TEXTURE_CLOCK,
XMB_TEXTURE_BATTERY_FULL,
XMB_TEXTURE_BATTERY_CHARGING,
XMB_TEXTURE_BATTERY_80,
XMB_TEXTURE_BATTERY_60,
XMB_TEXTURE_BATTERY_40,
XMB_TEXTURE_BATTERY_20,
XMB_TEXTURE_POINTER,
XMB_TEXTURE_ADD,
XMB_TEXTURE_KEY,
XMB_TEXTURE_KEY_HOVER,
XMB_TEXTURE_DIALOG_SLICE,
XMB_TEXTURE_ACHIEVEMENTS,
XMB_TEXTURE_AUDIO,
XMB_TEXTURE_EXIT,
XMB_TEXTURE_FRAMESKIP,
XMB_TEXTURE_INFO,
XMB_TEXTURE_HELP,
XMB_TEXTURE_NETWORK,
XMB_TEXTURE_POWER,
XMB_TEXTURE_SAVING,
XMB_TEXTURE_UPDATER,
XMB_TEXTURE_VIDEO,
XMB_TEXTURE_RECORD,
XMB_TEXTURE_INPUT_SETTINGS,
XMB_TEXTURE_MIXER,
XMB_TEXTURE_LOG,
XMB_TEXTURE_OSD,
XMB_TEXTURE_UI,
XMB_TEXTURE_USER,
XMB_TEXTURE_PRIVACY,
XMB_TEXTURE_LATENCY,
XMB_TEXTURE_DRIVERS,
XMB_TEXTURE_PLAYLIST,
XMB_TEXTURE_QUICKMENU,
XMB_TEXTURE_REWIND,
XMB_TEXTURE_OVERLAY,
XMB_TEXTURE_OVERRIDE,
XMB_TEXTURE_NOTIFICATIONS,
XMB_TEXTURE_STREAM,
XMB_TEXTURE_SHUTDOWN,
XMB_TEXTURE_INPUT_DPAD_U,
XMB_TEXTURE_INPUT_DPAD_D,
XMB_TEXTURE_INPUT_DPAD_L,
XMB_TEXTURE_INPUT_DPAD_R,
XMB_TEXTURE_INPUT_STCK_U,
XMB_TEXTURE_INPUT_STCK_D,
XMB_TEXTURE_INPUT_STCK_L,
XMB_TEXTURE_INPUT_STCK_R,
XMB_TEXTURE_INPUT_STCK_P,
XMB_TEXTURE_INPUT_SELECT,
XMB_TEXTURE_INPUT_START,
XMB_TEXTURE_INPUT_BTN_U,
XMB_TEXTURE_INPUT_BTN_D,
XMB_TEXTURE_INPUT_BTN_L,
XMB_TEXTURE_INPUT_BTN_R,
XMB_TEXTURE_INPUT_LB,
XMB_TEXTURE_INPUT_RB,
XMB_TEXTURE_INPUT_LT,
XMB_TEXTURE_INPUT_RT,
XMB_TEXTURE_INPUT_ADC,
XMB_TEXTURE_INPUT_BIND_ALL,
XMB_TEXTURE_INPUT_MOUSE,
XMB_TEXTURE_INPUT_LGUN,
XMB_TEXTURE_INPUT_TURBO,
XMB_TEXTURE_CHECKMARK,
XMB_TEXTURE_MENU_ADD,
XMB_TEXTURE_BRIGHTNESS,
XMB_TEXTURE_PAUSE,
XMB_TEXTURE_DEFAULT,
XMB_TEXTURE_DEFAULT_CONTENT,
XMB_TEXTURE_MENU_APPLY_TOGGLE,
XMB_TEXTURE_MENU_APPLY_COG,
XMB_TEXTURE_DISC,
XMB_TEXTURE_LAST
};
enum
{
XMB_SYSTEM_TAB_MAIN = 0,
XMB_SYSTEM_TAB_SETTINGS,
XMB_SYSTEM_TAB_HISTORY,
XMB_SYSTEM_TAB_FAVORITES,
XMB_SYSTEM_TAB_MUSIC,
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
XMB_SYSTEM_TAB_VIDEO,
#endif
#ifdef HAVE_IMAGEVIEWER
XMB_SYSTEM_TAB_IMAGES,
#endif
#ifdef HAVE_NETWORKING
XMB_SYSTEM_TAB_NETPLAY,
#endif
XMB_SYSTEM_TAB_ADD,
XMB_SYSTEM_TAB_CONTENTLESS_CORES,
#if defined(HAVE_LIBRETRODB)
XMB_SYSTEM_TAB_EXPLORE,
#endif
/* End of this enum - use the last one to determine num of possible tabs */
XMB_SYSTEM_TAB_MAX_LENGTH
};
enum xmb_pending_thumbnail_type
{
XMB_PENDING_THUMBNAIL_NONE = 0,
XMB_PENDING_THUMBNAIL_RIGHT,
XMB_PENDING_THUMBNAIL_LEFT,
XMB_PENDING_THUMBNAIL_BOTH,
XMB_PENDING_THUMBNAIL_ICONS
};
typedef struct
{
gfx_thumbnail_path_data_t thumbnail_path_data;
gfx_thumbnail_t icon;
} xmb_icons_t;
/* NOTE: If you change this you HAVE to update
* xmb_alloc_node().
*
* Layout rule: everything xmb_draw_item() reads for a visible entry
* lives at the front, so a node's entire per-frame working set is one
* 64-byte cacheline. One node is allocated per list entry, so a MAME
* or FBNeo playlist means tens of thousands of them; the draw path
* walks the visible window every frame and nothing else, so any cold
* member placed ahead of the hot ones costs a line fetch (and, once
* the node grows past a page, a dTLB entry) per entry per frame.
*
* thumbnail_icon is therefore a pointer, not a value. xmb_icons_t is
* ~15 KB (it embeds a whole gfx_thumbnail_path_data_t), which made the
* by-value node 15560 bytes -- 324x ozone_node_t and 65x
* materialui_node_t for the same job -- and put the hot floats in
* cachelines 242/243 of 244. It is now allocated on demand, only for
* entries the icon-thumbnail resolver in xmb_render() actually reaches
* (the visible window of a playlist), and released again by
* xmb_unload_icon_thumbnail_textures(). */
typedef struct
{
/* --- hot: xmb_draw_item(), every visible entry, every frame --- */
float alpha;
float label_alpha;
float zoom;
float x;
float y;
uintptr_t icon;
uintptr_t content_icon;
/* --- cold --- */
char *fullpath;
char *console_name;
xmb_icons_t *thumbnail_icon; /* lazily allocated, may be NULL */
} xmb_node_t;
enum xmb_drag_mode
{
XMB_DRAG_NONE = 0,
XMB_DRAG_DETECTING,
XMB_DRAG_HORIZONTAL,
XMB_DRAG_VERTICAL
};
typedef struct xmb_handle
{
/* Keeps track of the last time tabs were switched
* via a MENU_ACTION_LEFT/MENU_ACTION_RIGHT event */
retro_time_t last_tab_switch_time; /* uint64_t alignment */
char *box_message;
char *bg_file_path;
file_list_t horizontal_list; /* ptr alignment */
/* Maps console tabs to playlist database names */
xmb_node_t **playlist_db_node_map;
xmb_node_t main_menu_node;
#ifdef HAVE_IMAGEVIEWER
xmb_node_t images_tab_node;
#endif
xmb_node_t music_tab_node;
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
xmb_node_t video_tab_node;
#endif
xmb_node_t settings_tab_node;
xmb_node_t history_tab_node;
xmb_node_t favorites_tab_node;
xmb_node_t add_tab_node;
xmb_node_t contentless_cores_tab_node;
#if defined(HAVE_LIBRETRODB)
xmb_node_t explore_tab_node;
#endif
xmb_node_t netplay_tab_node;
menu_input_pointer_t pointer;
/* Touch drag state for direction locking */
enum xmb_drag_mode drag_mode;
float drag_start_x;
float drag_start_y;
float categories_drag_start_pos;
size_t drag_start_selection;
font_data_t *font;
font_data_t *font2;
video_font_raster_block_t raster_block;
video_font_raster_block_t raster_block2;
size_t (*word_wrap)(
char *s, size_t len,
const char *src, size_t src_len,
int line_width, int wideglyph_width, unsigned max_lines);
menu_screensaver_t *screensaver;
struct {
gfx_thumbnail_t right;
gfx_thumbnail_t left;
gfx_thumbnail_t icon;
gfx_thumbnail_t savestate;
enum xmb_pending_thumbnail_type pending;
enum xmb_pending_thumbnail_type pending_icons;
} thumbnails;
struct
{
uintptr_t bg;
uintptr_t list[XMB_TEXTURE_LAST];
} textures;
uintptr_t current_menu_icon;
size_t categories_selection_ptr;
size_t categories_selection_ptr_old;
size_t selection_ptr_old;
size_t fullscreen_thumbnail_selection;
/* size of the current list */
size_t list_size;
size_t tab_selection[XMB_TAB_MAX_LENGTH];
int depth;
int old_depth;
int icon_size;
int cursor_size;
int osk_textbox_x;
int osk_textbox_y;
int osk_textbox_w;
int osk_textbox_h;
int wideglyph_width;
/* The string wideglyph_width was measured from, and the
* font_driver generation it was measured at. A font rebuilt
* underneath - a language switch picks a different face - moves
* the generation and the width is worked out again. */
const char *wideglyph_str;
uint32_t wideglyph_generation;
unsigned categories_active_idx;
unsigned categories_active_idx_old;
unsigned ticker_limit;
unsigned draw_entry_delay;
float fullscreen_thumbnail_alpha;
float x;
float alpha;
float alpha_list;
float above_subitem_offset;
float above_item_offset;
float active_item_factor;
float under_item_offset;
float shadow_offset;
float font_size;
float font2_size;
float last_scale_factor;
/* Snapshot of menu_scale_factor clamped to a floor of 1, taken by
* xmb_layout() at the same time as scale_mod and the margins. The
* draw path must read this rather than the live setting, so that
* every scale-derived value on screen comes from one snapshot. */
float scale_cap;
float margins_screen_left;
float margins_screen_top;
float margins_setting_left;
float margins_title_left;
float margins_title_top;
float margins_title_bottom;
float margins_title;
float margins_title_horizontal_offset;
float last_margins_title;
float last_margins_title_horizontal_offset;
float margins_label_left;
float margins_label_top;
float icon_spacing_horizontal;
float icon_spacing_vertical;
float items_active_alpha;
float items_active_zoom;
float items_passive_alpha;
float items_passive_zoom;
float margins_dialog;
float margins_slice;
float categories_x_pos;
float categories_passive_alpha;
float categories_passive_zoom;
float categories_active_zoom;
float categories_active_alpha;
uint8_t system_tab_end;
uint8_t tabs[XMB_SYSTEM_TAB_MAX_LENGTH];
/* Frames remaining to retry xmb_set_title() after a context reset
* left an async-loaded sidebar / db-node icon unresolved. Capped
* so that a permanently-missing icon asset doesn't cause a
* perpetual retry loop. */
uint8_t current_menu_icon_retry;
char title_name[NAME_MAX_LENGTH];
char title_name_alt[NAME_MAX_LENGTH];
/* Cached texts showing current entry index / current list size */
char entry_index_str[32];
char entry_index_offset;
/* The menu font path the fonts are built from. xmb_render()
* watches it alongside the scale factor and schedules the same
* deferred rebuild. */
char last_font_path[PATH_MAX_LENGTH];
char savestate_thumbnail_file_path[PATH_MAX_LENGTH];
char fullscreen_thumbnail_label[NAME_MAX_LENGTH];
char thumbnails_left_status_prev;
char thumbnails_right_status_prev;
bool allow_horizontal_animation;
bool allow_dynamic_wallpaper;
bool fullscreen_thumbnails_available;
bool show_fullscreen_thumbnails;
bool want_fullscreen_thumbnails;
bool skip_thumbnail_reset;
/* Set by xmb_populate_entries when it wanted to run
* xmb_populate_dynamic_icons but deferred the work because the user
* is actively mashing / holding a navigation button
* (menu_st->scroll.acceleration > 0). xmb_render fires the actual
* work on a later frame, once input settles and acceleration drops
* to 0. This avoids the per-tab unload+path_data churn for
* intermediate tabs the user blows through during held-repeat tab
* traversal, without delaying the work behind any visible animation
* — the instant input lets go, the populate runs. */
bool pending_dynamic_icons_repopulate;
bool show_thumbnails;
bool show_mouse;
bool show_screensaver;
bool show_playlist_tabs;
bool show_horizontal_list;
bool use_ps3_layout;
bool last_use_ps3_layout;
bool assets_missing;
/* Favorites, History, Images, Music, Videos, user generated */
bool is_playlist;
bool is_playlist_tab;
bool is_playlist_information;
bool is_db_manager_list;
bool is_explore_list;
bool is_contentless_cores;
/* Load Content file browser */
bool is_file_list;
bool is_quick_menu;
bool is_state_slot;
bool libretro_running;
/* True from xmb_init() until the first xmb_frame() completes.
* Used to skip the toggle-fade-in that would otherwise render
* the very first frame at alpha=0 (a black screen) before the
* fade-in animation starts. */
bool is_first_frame;
/* Whether to show entry index for current list */
bool entry_idx_enabled;
/* Per-instance layout scale modifiers (was file-scope static) */
float scale_mod[8];
/* Base item color: 0,0,0 for inverted themes, 1,1,1 otherwise
* (was file-scope static xmb_item_color) */
float item_color_base[16];
/* Incremented on context destroy; the render path snapshots
* this at frame start and bails if it changes mid-frame.
* Prevents use-after-free on textures/fonts during driver
* reinit under threaded video. */
uint32_t context_generation;
/* Surface size last seen by xmb_frame. Updated every frame
* from video_info->{width,height} so non-render code paths
* (selection_pointer_changed, list_open_new, list_switch_new,
* list_cache, pointer_up, layout) can read the size without
* locking video_st via video_driver_get_output_size. */
unsigned last_width;
unsigned last_height;
/* Word-wrap scratch for the sublabel line ticker, grown on demand
* and kept for the menu's lifetime; it was malloc'd and freed on
* every frame the ticker ran. */
char *wrap_scratch;
size_t wrap_scratch_cap;
} xmb_handle_t;
/* Constant color templates — safe to share across threads.
* Mutable scratch copies are created on the stack where needed. */
static const float xmb_color_black[16] = {
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
};
static const float xmb_color_white[16] = {
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
1.0f, 1.0f, 1.0f, 1.0f,
};
/* Forward declarations */
extern int action_switch_thumbnail(const char *path,
const char *label, unsigned type, size_t idx);
static int xmb_menu_entry_action(void *userdata,
menu_entry_t *entry, size_t i, enum menu_action action);
static bool xmb_load_image(void *userdata, void *data,
enum menu_image_type type);
static void xmb_context_bg_destroy(xmb_handle_t *xmb);
static void xmb_navigation_set(void *data, bool scroll);
static uintptr_t xmb_icon_get_id(xmb_handle_t *xmb,
xmb_node_t *core_node, xmb_node_t *node,
enum msg_hash_enums enum_idx, const char *enum_path,
const char *enum_label, unsigned type, bool active,
bool checked);
static INLINE float xmb_item_y(const xmb_handle_t *xmb,
int i, size_t current)
{
float icon_spacing_vertical = xmb->icon_spacing_vertical;
if (i < (int)current)
{
if (!xmb->show_horizontal_list)
return icon_spacing_vertical * (i - (int)current + xmb->above_subitem_offset);
if (xmb->depth > 1)
return icon_spacing_vertical * (i - (int)current + xmb->above_subitem_offset);
return icon_spacing_vertical * (i - (int)current + xmb->above_item_offset);
}
else if (i == (int)current)
return icon_spacing_vertical * xmb->active_item_factor;
return icon_spacing_vertical * (i - (int)current + xmb->under_item_offset);
}
static void xmb_calculate_visible_range(const xmb_handle_t *xmb,
unsigned height, size_t list_size, unsigned current,
unsigned *first, unsigned *last)
{
unsigned j;
float base_y = xmb->margins_screen_top;
*first = 0;
*last = (unsigned)(list_size ? list_size - 1 : 0);
if (current)
{
for (j = current; j-- > 0;)
{
float bottom = xmb_item_y(xmb, j, current) + base_y + xmb->icon_size;
if (bottom < 0)
break;
*first = j;
}
}
for (j = current + 1; j < list_size; j++)
{
float top = xmb_item_y(xmb, j, current) + base_y;
if (top > height)
break;
*last = j;
}
}
const char* xmb_theme_ident(void)
{
unsigned menu_xmb_theme = config_get_ptr()->uints.menu_xmb_theme;
switch (menu_xmb_theme)
{
case XMB_ICON_THEME_FLATUI:
return "flatui";
case XMB_ICON_THEME_FLATUX:
return "flatux";
case XMB_ICON_THEME_RETROSYSTEM:
return "retrosystem";
case XMB_ICON_THEME_PIXEL:
return "pixel";
case XMB_ICON_THEME_SYSTEMATIC:
return "systematic";
case XMB_ICON_THEME_DOTART:
return "dot-art";
case XMB_ICON_THEME_CUSTOM:
return "custom";
case XMB_ICON_THEME_MONOCHROME_INVERTED:
return "monochrome";
case XMB_ICON_THEME_AUTOMATIC:
return "automatic";
case XMB_ICON_THEME_AUTOMATIC_INVERTED:
return "automatic";
case XMB_ICON_THEME_DAITE:
return "daite";
case XMB_ICON_THEME_MONOCHROME:
default:
break;
}
return "monochrome";
}
/* NOTE: This exists because calloc()ing xmb_node_t is expensive
* when you can have big lists like MAME and fba playlists.
*
* Per-field init only. The node itself is now small (one cacheline),
* but it is still allocated once per list entry, so this stays on the
* populate hot path and stays a malloc + explicit field init rather
* than a calloc.
*
* The multi-KB xmb_icons_t block that used to be inlined here is no
* longer allocated at all until something asks for it; see
* xmb_node_icons_get(). */
static xmb_node_t *xmb_alloc_node(void)
{
xmb_node_t *node = (xmb_node_t*)malloc(sizeof(*node));
if (!node)
return NULL;
node->alpha = node->label_alpha = 0;
node->zoom = node->x = node->y = 0;
node->icon = node->content_icon = 0;
node->fullpath = NULL;
node->console_name = NULL;
node->thumbnail_icon = NULL;
return node;
}
/* Lazily allocate a node's icon-thumbnail block.
*
* calloc() (not malloc + per-field init) is deliberate here: unlike
* xmb_alloc_node() this runs at most once per *visible* entry rather
* than once per list entry, so zeroing ~15 KB is not on any hot path,
* and it is the only way to guarantee the fields nothing in xmb.c
* writes are well-defined. The by-value layout this replaces left
* playlist_icon_mode, system[] and content_img[] uninitialised, which
* gfx_thumbnail_is_enabled() and the network-on-demand branch of
* gfx_thumbnail_request() both read.
*
* gfx_thumbnail_init_blank() is still required on top of the calloc:
* gfx_thumbnail_t.status is atomically-typed and must be initialised
* through retro_atomic_int_init() for the first write to be
* well-defined under C11 stdatomic and C++11 std::atomic. */
static xmb_icons_t *xmb_node_icons_get(xmb_node_t *node)
{
xmb_icons_t *icons;
if (!node)
return NULL;
if (node->thumbnail_icon)
return node->thumbnail_icon;
if (!(icons = (xmb_icons_t*)calloc(1, sizeof(*icons))))
return NULL;
gfx_thumbnail_init_blank(&icons->icon);
node->thumbnail_icon = icons;
return icons;
}
/* Release a node's icon-thumbnail block, resetting the thumbnail
* first so any live texture/animation is torn down. Safe to call on
* a node that never had one. */
static void xmb_node_icons_free(xmb_node_t *node)
{
if (!node || !node->thumbnail_icon)
return;
gfx_thumbnail_reset(&node->thumbnail_icon->icon);
free(node->thumbnail_icon);
node->thumbnail_icon = NULL;
}
static void xmb_free_node(xmb_node_t *node)
{
if (!node)
return;
/* Shared with every other node of the same list; released by
* reference, never with free(). */
if (node->fullpath)
menu_str_unref(node->fullpath);
node->fullpath = NULL;
xmb_node_icons_free(node);
free(node);
}
/* file_list_t::userdata_free hook. Installed wherever a node is put
* into a list, so that a list holding one knows how to take it apart
* however it is destroyed -- including file_list_pop() and the bare
* file_list_deinitialize() paths, which reached free() directly and
* leaked whatever the node owned. */
static void xmb_free_node_cb(void *userdata)
{
xmb_free_node((xmb_node_t*)userdata);
}
/**
* @brief frees all xmb_node_t in a file_list_t
*
* file_list_t assumes userdata holds a simple structure and
* free()'s it. Can't change this at the time because other
* code depends on this behavior.
*
* @param list
* @param actiondata whether to free actiondata too
*/
static void xmb_free_list_nodes(file_list_t *list, bool actiondata)
{
unsigned i;
unsigned size = list ? (unsigned)list->size : 0;
uintptr_t tag = (uintptr_t)list;
/* Kill any in-flight tweens whose subject points into a node we are
* about to free. Horizontal-list node highlight animations are tagged
* with the address of their owning file_list_t (see
* xmb_list_open_horizontal_list / xmb_list_switch_horizontal_list);
* without this the next gfx_animation_update() would write through a
* freed node->alpha. No-op when nothing is tagged for this list. */
gfx_animation_kill_by_tag(&tag);
for (i = 0; i < size; ++i)
{
xmb_free_node((xmb_node_t*)file_list_get_userdata_at_offset(list, i));
list->list[i].userdata = NULL;
if (actiondata)
file_list_free_actiondata(list, i);
}
}
static float *xmb_gradient_ident(unsigned xmb_color_theme)
{
static float gradient_golden[16] = {
174/255.0, 123/255.0, 44/255.0, 1.0,
205/255.0, 174/255.0, 84/255.0, 1.0,
58/255.0, 43/255.0, 24/255.0, 1.0,
58/255.0, 43/255.0, 24/255.0, 1.0,
};
static float gradient_legacy_red[16] = {
171/255.0, 70/255.0, 59/255.0, 1.0,
171/255.0, 70/255.0, 59/255.0, 1.0,
190/255.0, 80/255.0, 69/255.0, 1.0,
190/255.0, 80/255.0, 69/255.0, 1.0,
};
static float gradient_electric_blue[16] = {
1/255.0, 2/255.0, 67/255.0, 1.0,
1/255.0, 73/255.0, 183/255.0, 1.0,
1/255.0, 93/255.0, 194/255.0, 1.0,
3/255.0, 162/255.0, 254/255.0, 1.0,
};
static float gradient_dark_purple[16] = {
20/255.0, 13/255.0, 20/255.0, 1.0,
20/255.0, 13/255.0, 20/255.0, 1.0,
92/255.0, 44/255.0, 92/255.0, 1.0,
148/255.0, 90/255.0, 148/255.0, 1.0,
};
static float gradient_midnight_blue[16] = {
44/255.0, 62/255.0, 80/255.0, 1.0,
44/255.0, 62/255.0, 80/255.0, 1.0,
44/255.0, 62/255.0, 80/255.0, 1.0,
44/255.0, 62/255.0, 80/255.0, 1.0,
};
static float gradient_apple_green[16] = {
102/255.0, 134/255.0, 58/255.0, 1.0,
122/255.0, 131/255.0, 52/255.0, 1.0,
82/255.0, 101/255.0, 35/255.0, 1.0,
63/255.0, 95/255.0, 30/255.0, 1.0,
};
static float gradient_undersea[16] = {
23/255.0, 18/255.0, 41/255.0, 1.0,
30/255.0, 72/255.0, 114/255.0, 1.0,
52/255.0, 88/255.0, 110/255.0, 1.0,
69/255.0, 125/255.0, 140/255.0, 1.0,
};
static float gradient_morning_blue[16] = {
221/255.0, 241/255.0, 254/255.0, 1.0,
135/255.0, 206/255.0, 250/255.0, 1.0,
0.7, 0.7, 0.7, 1.0,
170/255.0, 200/255.0, 252/255.0, 1.0,
};
static float gradient_sunbeam[16] = {
20/255.0, 13/255.0, 20/255.0, 1.0,
30/255.0, 72/255.0, 114/255.0, 1.0,
0.7, 0.7, 0.7, 1.0,
0.1, 0.0, 0.1, 1.0,
};
static float gradient_lime_green[16] = {
209/255.0, 255/255.0, 82/255.0, 1.0,
146/255.0, 232/255.0, 66/255.0, 1.0,
82/255.0, 101/255.0, 35/255.0, 1.0,
63/255.0, 95/255.0, 30/255.0, 1.0,
};
static float gradient_pikachu_yellow[16] = {
63/255.0, 63/255.0, 1/255.0, 1.0,
174/255.0, 174/255.0, 1/255.0, 1.0,
191/255.0, 194/255.0, 1/255.0, 1.0,
254/255.0, 221/255.0, 3/255.0, 1.0,
};
static float gradient_gamecube_purple[16] = {
40/255.0, 20/255.0, 91/255.0, 1.0,
160/255.0, 140/255.0, 211/255.0, 1.0,
107/255.0, 92/255.0, 177/255.0, 1.0,
84/255.0, 71/255.0, 132/255.0, 1.0,
};
static float gradient_famicom_red[16] = {
255/255.0, 191/255.0, 171/255.0, 1.0,
119/255.0, 49/255.0, 28/255.0, 1.0,
148/255.0, 10/255.0, 36/255.0, 1.0,
206/255.0, 126/255.0, 110/255.0, 1.0,
};
static float gradient_flaming_hot[16] = {
231/255.0, 53/255.0, 53/255.0, 1.0,
242/255.0, 138/255.0, 97/255.0, 1.0,
236/255.0, 97/255.0, 76/255.0, 1.0,
255/255.0, 125/255.0, 3/255.0, 1.0,
};
static float gradient_ice_cold[16] = {
66/255.0, 183/255.0, 229/255.0, 1.0,
29/255.0, 164/255.0, 255/255.0, 1.0,
176/255.0, 255/255.0, 247/255.0, 1.0,
174/255.0, 240/255.0, 255/255.0, 1.0,
};
static float gradient_midgar[16] = {
255/255.0, 0/255.0, 0/255.0, 1.0,
0/255.0, 0/255.0, 255/255.0, 1.0,
0/255.0, 255/255.0, 0/255.0, 1.0,
32/255.0, 32/255.0, 32/255.0, 1.0,
};
static float gradient_volcanic_red[16] = {
1.0, 0.0, 0.1, 1.0,
1.0, 0.1, 0.0, 1.0,
0.1, 0.0, 0.1, 1.0,
0.1, 0.0, 0.1, 1.0,
};
static float gradient_dark[16] = {
0.05, 0.05, 0.05, 1.0,
0.05, 0.05, 0.05, 1.0,
0.05, 0.05, 0.05, 1.0,
0.05, 0.05, 0.05, 1.0,
};
static float gradient_light[16] = {
0.50, 0.50, 0.50, 1.0,
0.50, 0.50, 0.50, 1.0,
0.50, 0.50, 0.50, 1.0,
0.50, 0.50, 0.50, 1.0,
};
static float gradient_gray_dark[16] = {
16/255.0, 16/255.0, 16/255.0, 1.0,
16/255.0, 16/255.0, 16/255.0, 1.0,
16/255.0, 16/255.0, 16/255.0, 1.0,
16/255.0, 16/255.0, 16/255.0, 1.0,
};
static float gradient_gray_light[16] = {
32/255.0, 32/255.0, 32/255.0, 1.0,
32/255.0, 32/255.0, 32/255.0, 1.0,
32/255.0, 32/255.0, 32/255.0, 1.0,
32/255.0, 32/255.0, 32/255.0, 1.0,
};
switch (xmb_color_theme)
{
case XMB_THEME_DARK_PURPLE:
return &gradient_dark_purple[0];
case XMB_THEME_MIDNIGHT_BLUE:
return &gradient_midnight_blue[0];
case XMB_THEME_GOLDEN:
return &gradient_golden[0];
case XMB_THEME_ELECTRIC_BLUE:
return &gradient_electric_blue[0];
case XMB_THEME_APPLE_GREEN:
return &gradient_apple_green[0];
case XMB_THEME_UNDERSEA:
return &gradient_undersea[0];
case XMB_THEME_VOLCANIC_RED:
return &gradient_volcanic_red[0];
case XMB_THEME_DARK:
return &gradient_dark[0];
case XMB_THEME_LIGHT:
return &gradient_light[0];
case XMB_THEME_MORNING_BLUE:
return &gradient_morning_blue[0];
case XMB_THEME_SUNBEAM:
return &gradient_sunbeam[0];
case XMB_THEME_LIME:
return &gradient_lime_green[0];
case XMB_THEME_MIDGAR:
return &gradient_midgar[0];
case XMB_THEME_PIKACHU_YELLOW:
return &gradient_pikachu_yellow[0];
case XMB_THEME_GAMECUBE_PURPLE:
return &gradient_gamecube_purple[0];
case XMB_THEME_FAMICOM_RED:
return &gradient_famicom_red[0];
case XMB_THEME_FLAMING_HOT:
return &gradient_flaming_hot[0];
case XMB_THEME_ICE_COLD:
return &gradient_ice_cold[0];
case XMB_THEME_LEGACY_RED:
return &gradient_legacy_red[0];
case XMB_THEME_GRAY_DARK:
return &gradient_gray_dark[0];
case XMB_THEME_GRAY_LIGHT:
return &gradient_gray_light[0];
default:
return &gradient_dark[0];
}
}
static size_t xmb_list_get_selection(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (!xmb)
return 0;
return xmb->categories_selection_ptr;
}
static size_t xmb_list_get_size(void *data,
enum menu_list_type type)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
switch (type)
{
case MENU_LIST_PLAIN:
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
if (menu_list)
return MENU_LIST_GET_STACK_SIZE(menu_list, 0);
}
break;
case MENU_LIST_HORIZONTAL:
return xmb->horizontal_list.size;
case MENU_LIST_TABS:
return xmb->system_tab_end;
default:
break;
}
return 0;
}
static void *xmb_list_get_entry(void *data,
enum menu_list_type type, unsigned i)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
size_t list_size = 0;
switch (type)
{
case MENU_LIST_PLAIN:
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET_SELECTION(menu_list, 0);
list_size = MENU_LIST_GET_STACK_SIZE(menu_list, 0);
if (i < list_size)
return (void*)&menu_stack->list[i];
}
break;
case MENU_LIST_HORIZONTAL:
list_size = xmb->horizontal_list.size;
if (i < list_size)
return (void*)&xmb->horizontal_list.list[i];
break;
default:
break;
}
return NULL;
}
static void xmb_draw_icon(
void *userdata,
gfx_display_t *p_disp,
gfx_display_ctx_driver_t *dispctx,
unsigned video_width,
unsigned video_height,
bool shadows_enable,
int icon_size_x,
int icon_size_y,
uintptr_t texture,
float x,
float y,
unsigned width,
unsigned height,
float alpha,
float rotation,
float scale_factor,
float *color,
float shadow_offset,
math_matrix_4x4 *mymat)
{
gfx_display_ctx_draw_t draw;
struct video_coords coords;
math_matrix_4x4 mymat_tmp;
/* Skip drawing when the texture hasn't loaded yet
* (async loads start at 0 and are written on completion) */
if (!texture)
return;
if ( (x < (-icon_size_x / 2.0f))
|| (x > width)
|| (y < (icon_size_y / 2.0f))
|| (y > height + icon_size_y)
)
return;
/* Every draw below goes through dispctx; without one there is
* nothing to render (the context is torn down around a video
* driver reinit while cached frames may still be submitted). */
if (!dispctx)
return;
if (!dispctx->handles_transform)
{
float radians = rotation;
float cosine = cosf(radians);
float sine = sinf(radians);
gfx_display_rotate_z(p_disp, &mymat_tmp, cosine, sine, userdata);
if (scale_factor != 1.0f)
{
math_matrix_4x4 matrix_scaled = {
{ 0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 1.0f }
};
MAT_ELEM_4X4(matrix_scaled, 0, 0) = scale_factor;
MAT_ELEM_4X4(matrix_scaled, 1, 1) = scale_factor;
MAT_ELEM_4X4(matrix_scaled, 2, 2) = 1.0f;
matrix_4x4_multiply(mymat_tmp, matrix_scaled, mymat_tmp);
}
}
else if (mymat)
mymat_tmp = *mymat;
coords.vertices = 4;
coords.vertex = NULL;
coords.tex_coord = NULL;
coords.lut_tex_coord = NULL;
draw.width = icon_size_x;
draw.height = icon_size_y;
draw.rotation = rotation;
draw.scale_factor = scale_factor;
#if defined(VITA) || defined(WIIU) || defined(__PS3__)
draw.width *= scale_factor;
draw.height *= scale_factor;
#endif
draw.coords = &coords;
draw.matrix_data = &mymat_tmp;
draw.texture = texture;
draw.pipeline_id = 0;
if (shadows_enable)
{
float shadow_color[16] = {
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
0.0f, 0.0f, 0.0f, 0.0f,
};
gfx_display_set_alpha(shadow_color, color[3] * GFX_SHADOW_ALPHA * 0.75f);
coords.color = shadow_color;
draw.x = x + shadow_offset;
draw.y = height - y - shadow_offset;
#if defined(VITA) || defined(WIIU) || defined(__PS3__)
if (scale_factor < 1)
{
draw.x = draw.x + (icon_size_x-draw.width)/2;
draw.y = draw.y + (icon_size_y-draw.width)/2;
}
#endif
if (draw.height > 0 && draw.width > 0)
if (dispctx && dispctx->draw)
dispctx->draw(&draw, userdata, video_width, video_height);
}
coords.color = (const float*)color;
draw.x = x;
draw.y = height - y;
#if defined(VITA) || defined(WIIU) || defined(__PS3__)
if (scale_factor < 1)
{
draw.x = draw.x + (icon_size_x-draw.width)/2;
draw.y = draw.y + (icon_size_y-draw.width)/2;
}
#endif
if (draw.height > 0 && draw.width > 0)
if (dispctx && dispctx->draw)
dispctx->draw(&draw, userdata, video_width, video_height);
}
static void xmb_draw_text(
bool shadows_enable,
xmb_handle_t *xmb,
settings_t *settings,
const char *str,
float x, float y,
float scale_factor,
float alpha,
enum text_alignment text_align,
unsigned width, unsigned height,
font_data_t* font)
{
uint32_t color;
uint8_t a8;
float alpha_hp;
float color_hp[4];
if (alpha > xmb->alpha)
alpha = xmb->alpha;
a8 = 0xFF * alpha;
alpha_hp = alpha;
/* Avoid drawing 100% transparent text */
if (a8 == 0)
return;
if ( !strcmp(str, "null")
|| !strcmp(str, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_OFF))
|| !strcmp(str, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MORE)))
{
a8 = 0x7F * alpha;
alpha_hp = alpha * (127.0f / 255.0f);
}
color = FONT_COLOR_RGBA(
settings->uints.menu_font_color_red,
settings->uints.menu_font_color_green,
settings->uints.menu_font_color_blue, a8);
/* Full-precision copy of the same colour: the RGB is the user's 8-bit
* menu_font_color_* setting, but the alpha (which XMB animates as a float
* for fade in/out) is passed un-quantised so the fade is not capped at
* 256 steps on a deep-colour framebuffer. Backends that ignore the
* high-precision path fall back to 'color', so the 8-bit result is
* unchanged there. */
color_hp[0] = settings->uints.menu_font_color_red * (1.0f / 255.0f);
color_hp[1] = settings->uints.menu_font_color_green * (1.0f / 255.0f);
color_hp[2] = settings->uints.menu_font_color_blue * (1.0f / 255.0f);
color_hp[3] = alpha_hp < 0.0f ? 0.0f : (alpha_hp > 1.0f ? 1.0f : alpha_hp);
gfx_display_draw_text_hp(font, str, x, y,
width, height, color, color_hp, text_align, scale_factor,
shadows_enable,
xmb->shadow_offset, false);
}
static void xmb_messagebox(void *data, const char *message)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (xmb && message && *message)
{
/* Free any previously-set box_message before overwriting.
* The render path at line ~9215 consumes box_message
* exactly once (strlcpy to stack msg, then free + NULL),
* but nothing prevents xmb_messagebox from being called
* twice between renders (e.g. rapid error notifications,
* or a second messagebox triggered by a background task
* completion before the menu has repainted). Pre-patch
* the second strdup leaked the first one. */
if (xmb->box_message)
free(xmb->box_message);
xmb->box_message = strdup(message);
}
}
XMB_NOINLINE static void xmb_render_messagebox_internal(
void *userdata,
gfx_display_t *p_disp,
gfx_display_ctx_driver_t *dispctx,
unsigned video_width,
unsigned video_height,
xmb_handle_t *xmb,
const char *message,
bool draw_caret,
math_matrix_4x4 *mymat)
{
unsigned i, line_count = 0;
int x, y = 0;
float line_height = 0;
int longest_width = 0;
int usable_width = 0;
unsigned y_position = 0;
int slice_x = 0;
int slice_y = 0;
int slice_w = 0;
int slice_h = 0;
unsigned cursor_line = 0;
int cursor_x = 0;
struct menu_state *menu_st = menu_state_get_ptr();
bool confirm_dialog = (menu_st->dialog_st.confirm_cmd) ? true : false;
bool input_dialog_display_kb = false;
size_t wrapped_len = 0;
char wrapped_message[MENU_LABEL_MAX_LENGTH];
char *lines[512];
char *start, *nl, *end;
if ((usable_width = (int)video_width - (xmb->margins_dialog * 8)) < 1)
return;
usable_width = (usable_width < 300) ? 300 : usable_width;
wrapped_len = (xmb->word_wrap)(
wrapped_message, sizeof(wrapped_message),
message, strlen(message),
usable_width / (xmb->font_size * 0.85f),
xmb->wideglyph_width, 0);
if (wrapped_len == 0)
return;
/* Single-pass: find lines, null-terminate, measure width */
start = wrapped_message;
end = wrapped_message + wrapped_len;
while (start < end && line_count < 512)
{
nl = (char *)memchr(start, '\n', end - start);
if (nl)
*nl = '\0';
lines[line_count++] = start;
if (*start)
{
int msg_width = font_driver_get_message_width(
xmb->font, start, (nl ? nl : end) - start, 1.0f);
if (msg_width > longest_width)
longest_width = msg_width;
if (longest_width > usable_width)
longest_width = usable_width;
}
start = nl ? nl + 1 : end + 1;
}
if (line_count == 0)
return;
input_dialog_display_kb = menu_input_dialog_get_display_kb();
line_height = xmb->font->size * 1.30f;
y_position = (input_dialog_display_kb)
? video_height / 4 : video_height / 2;
x = video_width / 2;
y = y_position - (line_count - 1) * line_height / 2;
/* Narrow font can make the box too narrow for Back & OK */
if (confirm_dialog && longest_width < (int)video_width / 4)
longest_width = video_width / 4;
if (draw_caret)
{
input_driver_state_t *input_st = input_state_get_ptr();
input_keyboard_line_t *line = &input_st->keyboard_line;
const char *input = strchr(message, '\n');
draw_caret = false;
if (input && line->buffer
&& ((menu_driver_get_current_time() / 500000) & 1))
{
char cursor_src[MENU_LABEL_MAX_LENGTH];
char cursor_message[MENU_LABEL_MAX_LENGTH];
size_t len = (size_t)(input - message + 1);
size_t ptr = line->ptr;
if (ptr > line->size)
ptr = line->size;
if (len < sizeof(cursor_src))
{
if (ptr >= sizeof(cursor_src) - len)
ptr = sizeof(cursor_src) - len - 1;
/* word_wrap()/word_wrap_wideglyph() require
* non-overlapping source and destination
* buffers, so stage the source separately */
memcpy(cursor_src, message, len);
memcpy(cursor_src + len, line->buffer, ptr);
cursor_src[len + ptr] = '\0';
(xmb->word_wrap)(
cursor_message, sizeof(cursor_message),
cursor_src, len + ptr,
usable_width / (xmb->font_size * 0.85f),
xmb->wideglyph_width, 0);
input = cursor_message;
while ((input = strchr(input, '\n')))
{
cursor_line++;
input++;
}
input = strrchr(cursor_message, '\n');
input = input ? input + 1 : cursor_message;
cursor_x = font_driver_get_message_width(xmb->font, input, strlen(input), 1.0f);
draw_caret = true;
}
}
}
slice_x = x - (longest_width / 2) - xmb->margins_dialog;
slice_y = y + xmb->margins_slice - xmb->margins_dialog;
slice_w = longest_width + (xmb->margins_dialog * 2);
slice_h = (line_height * line_count) + (xmb->margins_dialog * 2);
/* Extra room for confirm buttons */
if (confirm_dialog)
slice_h += (line_height * 2);
if (input_dialog_display_kb)
{
xmb->osk_textbox_x = slice_x;
xmb->osk_textbox_y = slice_y;
xmb->osk_textbox_w = slice_w;
xmb->osk_textbox_h = slice_h;
}
if (input_dialog_display_kb && input_state_get_ptr()->osk_textbox_focus && line_count > 1)
{
int cursor_offset = (xmb->margins_dialog + (xmb->margins_slice * 2)) / 3;
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
gfx_display_draw_texture_slice(
p_disp,
userdata,
video_width,
video_height,
slice_x - cursor_offset,
slice_y - cursor_offset,
256,
256,
slice_w + (cursor_offset * 2),
slice_h + (cursor_offset * 2),
video_width,
video_height,
NULL,
xmb->margins_slice,
xmb->last_scale_factor,
xmb->textures.list[XMB_TEXTURE_KEY_HOVER],
mymat);
if (dispctx && dispctx->blend_end)
dispctx->blend_end(userdata);
}
if (!xmb->assets_missing)
{
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
gfx_display_draw_texture_slice(
p_disp,
userdata,
video_width,
video_height,
slice_x,
slice_y,
256,
256,
slice_w,
slice_h,
video_width,
video_height,
NULL,
xmb->margins_slice,
xmb->last_scale_factor,
xmb->textures.list[XMB_TEXTURE_DIALOG_SLICE],
mymat);
if (dispctx && dispctx->blend_end)
dispctx->blend_end(userdata);
}
else
{
/* Fallback background without assets */
float frame_color[16] = {
0.85f, 0.85f, 0.85f, 1.0f,
0.85f, 0.85f, 0.85f, 1.0f,
0.85f, 0.85f, 0.85f, 1.0f,
0.85f, 0.85f, 0.85f, 1.0f,
};
slice_h -= line_height / 2;
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
slice_x,
slice_y,
slice_w,
slice_h,
video_width,
video_height,
frame_color,
NULL);
}
for (i = 0; i < line_count; i++)
{
const char *msg = lines[i];
if (msg)
gfx_display_draw_text(xmb->font, msg,
x - (longest_width / 2.0),
y + ((i + 0.85) * line_height),
video_width, video_height, 0x444444ff,
TEXT_ALIGN_LEFT, 1.0f, false, 0.0f, false);
}
if (draw_caret && (cursor_line < line_count))
{
float caret_color[16] = {
0.27f, 0.27f, 0.27f, 1.0f,
0.27f, 0.27f, 0.27f, 1.0f,
0.27f, 0.27f, 0.27f, 1.0f,
0.27f, 0.27f, 0.27f, 1.0f,
};
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
x - (longest_width / 2.0) + cursor_x,
y + ((cursor_line + 0.85) * line_height) - xmb->font->size,
2,
xmb->font->size,
video_width,
video_height,
caret_color,
NULL);
}
if (input_dialog_display_kb)
{
input_driver_state_t *input_st = input_state_get_ptr();
gfx_display_draw_keyboard(
p_disp,
userdata,
video_width,
video_height,
xmb->textures.list[XMB_TEXTURE_KEY_HOVER],
xmb->font,
input_st->osk_grid,
input_st->osk_textbox_focus ? 44 : input_st->osk_ptr,
0xffffffff);
}
if (confirm_dialog)
{
float frame_color[16] = {
0.75f, 0.75f, 0.75f, 0.5f,
0.75f, 0.75f, 0.75f, 0.5f,
0.75f, 0.75f, 0.75f, 0.5f,
0.75f, 0.75f, 0.75f, 0.5f,
};
settings_t *settings = config_get_ptr();
bool input_menu_swap_ok_cancel_buttons = settings->bools.input_menu_swap_ok_cancel_buttons;
const char *str_back = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_BASIC_MENU_CONTROLS_BACK);
const char *str_ok = msg_hash_to_str(MENU_ENUM_LABEL_VALUE_BASIC_MENU_CONTROLS_OK);
unsigned str_back_width = font_driver_get_message_width(xmb->font, str_back, strlen(str_back), 1.0f);
unsigned str_ok_width = font_driver_get_message_width(xmb->font, str_ok, strlen(str_ok), 1.0f);
float *col = xmb->item_color_base;
float scale_factor = xmb->last_scale_factor;
float icon_size = 50 * scale_factor;
float icon_padding = 10 * scale_factor;
float icon_x = x - (slice_w / 2) + icon_size + (icon_padding * 4);
float icon_y = video_height - y + (slice_h / 2) + (xmb->margins_slice / 2);
float label_y = icon_y - (line_height / 2.2f);
int cursor_x = icon_x - icon_padding;
int cursor_y = icon_y - icon_padding - icon_size;
int cursor_w = icon_size + (icon_padding * 4) + str_back_width;
int cursor_h = icon_size + (icon_padding * 2);
gfx_display_set_alpha(col, 0.5f);
/* Back */
if ( xmb->pointer.x >= cursor_x
&& xmb->pointer.x <= cursor_x + cursor_w
&& xmb->pointer.y >= cursor_y
&& xmb->pointer.y <= cursor_y + cursor_h)
{
menu_st->dialog_st.confirm_hover_back = true;
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
cursor_x,
cursor_y,
cursor_w,
cursor_h,
video_width,
video_height,
frame_color,
NULL);
}
else
menu_st->dialog_st.confirm_hover_back = false;
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
true,
icon_size,
icon_size,
input_menu_swap_ok_cancel_buttons
? xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R]
: xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D],
icon_x,
icon_y,
video_width,
video_height,
xmb->alpha,
0,
1,
col,
xmb->shadow_offset / 2,
mymat);
gfx_display_draw_text(
xmb->font2,
str_back,
icon_x + icon_size + icon_padding,
label_y,
video_width,
video_height,
0x444444ff,
TEXT_ALIGN_LEFT,
1.0f,
false,
1.0f,
false);
if (dispctx->blend_end)
dispctx->blend_end(userdata);
/* OK */
icon_x += slice_w - (icon_size * 2) - (icon_padding * 8) - xmb->margins_dialog - str_ok_width;
cursor_x = icon_x - icon_padding;
cursor_w = icon_size + (icon_padding * 4) + str_ok_width;
if ( xmb->pointer.x >= cursor_x
&& xmb->pointer.x <= cursor_x + cursor_w
&& xmb->pointer.y >= cursor_y
&& xmb->pointer.y <= cursor_y + cursor_h)
{
menu_st->dialog_st.confirm_hover_ok = true;
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
cursor_x,
cursor_y,
cursor_w,
cursor_h,
video_width,
video_height,
frame_color,
NULL);
}
else
menu_st->dialog_st.confirm_hover_ok = false;
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
true,
icon_size,
icon_size,
input_menu_swap_ok_cancel_buttons
? xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D]
: xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R],
icon_x,
icon_y,
video_width,
video_height,
xmb->alpha,
0,
1,
col,
xmb->shadow_offset / 2,
mymat);
gfx_display_draw_text(
xmb->font2,
str_ok,
icon_x + icon_size + icon_padding,
label_y,
video_width,
video_height,
0x444444ff,
TEXT_ALIGN_LEFT,
1.0f,
false,
1.0f,
false);
if (dispctx->blend_end)
dispctx->blend_end(userdata);
}
}
static bool xmb_osk_pointer_over_textbox(
void *data,
int x,
int y,
unsigned width,
unsigned height)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if ( xmb
&& menu_input_dialog_get_display_kb()
&& x > xmb->osk_textbox_x
&& x < xmb->osk_textbox_x + xmb->osk_textbox_w
&& y > xmb->osk_textbox_y
&& y < xmb->osk_textbox_y + xmb->osk_textbox_h)
return true;
return false;
}
#ifdef HAVE_LIBRETRODB
static bool xmb_is_main_menu_explore(void)
{
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
menu_entry_get(&entry, 0, 0, NULL, true);
return entry.type == FILE_TYPE_RDB;
}
#endif
static void xmb_path_dynamic_wallpaper(xmb_handle_t *xmb, char *s, size_t len)
{
settings_t *settings = config_get_ptr();
bool menu_dynamic_wallpaper_enable = settings->bools.menu_dynamic_wallpaper_enable;
unsigned xmb_color_theme = settings->uints.menu_xmb_color_theme;
const char *path_menu_wallpaper = settings->paths.path_menu_wallpaper;
const char *dir_dynamic_wallpapers = settings->paths.directory_dynamic_wallpapers;
unsigned depth = (unsigned)xmb_list_get_size(xmb, MENU_LIST_PLAIN);
/* Dynamic wallpaper takes precedence as reset background,
* then comes 'menu_wallpaper', and then iconset 'bg.png' */
if (menu_dynamic_wallpaper_enable)
{
size_t _len = fill_pathname_join_special(s,
dir_dynamic_wallpapers,
xmb->title_name,
len);
strlcpy_lit(s + _len, ".png", len - _len);
}
if (s && *s && path_is_valid(s))
;/* no-op */
else if (path_menu_wallpaper && *path_menu_wallpaper)
strlcpy(s, path_menu_wallpaper, len);
else if (xmb_color_theme == XMB_THEME_WALLPAPER)
fill_pathname_application_special(s, len,
APPLICATION_SPECIAL_DIRECTORY_ASSETS_XMB_BG);
/* Keep previous wallpaper in playlists and explore lists */
if ( (xmb->categories_selection_ptr == XMB_SYSTEM_TAB_MAIN && depth > 3)
|| (xmb->categories_selection_ptr > xmb->system_tab_end && depth > 1)
#ifdef HAVE_LIBRETRODB
|| (xmb->categories_selection_ptr == xmb->system_tab_end && menu_explore_is_content_list())
|| (xmb->is_explore_list && !menu_explore_is_content_list() && !xmb_is_main_menu_explore())
#endif
)
{
if ((xmb->bg_file_path && *xmb->bg_file_path) && !path_is_valid(s))
strlcpy(s, xmb->bg_file_path, len);
}
}
static void xmb_update_dynamic_wallpaper(xmb_handle_t *xmb, bool reset)
{
char path[PATH_MAX_LENGTH];
path[0] = '\0';
xmb_path_dynamic_wallpaper(xmb, path, sizeof(path));
if (!string_is_equal(path, xmb->bg_file_path) || reset)
{
if (path_is_valid(path))
{
if (reset)
{
xmb_context_bg_destroy(xmb);
if (!gfx_display_reset_icon_texture(path,
&xmb->textures.bg, gfx_display_texture_filter(),
NULL, NULL))
task_push_image_load(path,
(video_driver_get_disp_flags() & VIDEO_FLAG_USE_RGBA), 0,
0,
menu_display_handle_wallpaper_upload, NULL);
gfx_display_init_white_texture();
}
else
task_push_image_load(path,
(video_driver_get_disp_flags() & VIDEO_FLAG_USE_RGBA), 0,
0,
menu_display_handle_wallpaper_upload, NULL);
free(xmb->bg_file_path);
xmb->bg_file_path = strdup(path);
}
else
{
xmb_load_image(xmb, NULL, MENU_IMAGE_NONE);
free(xmb->bg_file_path);
xmb->bg_file_path = NULL;
}
}
}
static void xmb_update_savestate_thumbnail_path(void *data, unsigned i)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
settings_t *settings = config_get_ptr();
bool savestate_thumbnail;
/* Snapshot the current path on the stack before we clear it, so we
* can compare against the new path at the end to decide whether to
* reset the thumbnail cache. Previously this was a heap strdup
* assigned to a const char * that was never freed (leak on every
* selection change in the state-slot menu), performed BEFORE the
* !xmb NULL check below (null-deref if data was ever NULL), and
* used heap allocation for a value that lives in a fixed-size
* char[PATH_MAX_LENGTH] ivar. Same fix pattern as the materialui
* equivalent (93449d3): stack buffer, after the NULL guard. */
char old_path[PATH_MAX_LENGTH];
if (!xmb)
return;
savestate_thumbnail = settings->bools.savestate_thumbnail_enable;
strlcpy(old_path, xmb->savestate_thumbnail_file_path, sizeof(old_path));
if (xmb->skip_thumbnail_reset)
return;
xmb->savestate_thumbnail_file_path[0] = '\0';
/* Savestate thumbnails are only relevant
* when viewing the running quick menu or state slots */
if (!((xmb->is_quick_menu && menu_is_running_quick_menu()) || xmb->is_state_slot))
return;
xmb->fullscreen_thumbnails_available = false;
if (savestate_thumbnail)
{
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
entry.flags |= MENU_ENTRY_FLAG_LABEL_ENABLED;
menu_entry_get(&entry, 0, i, NULL, true);
if (*entry.label)
{
unsigned _state_slot = string_to_unsigned(entry.label);
if ( _state_slot == MENU_ENUM_LABEL_STATE_SLOT
|| string_is_equal(entry.label, MENU_ENUM_LABEL_STATE_SLOT_RUN_STR)
|| string_is_equal(entry.label, MENU_ENUM_LABEL_STATE_SLOT_STR)
|| string_is_equal(entry.label, MENU_ENUM_LABEL_LOAD_STATE_STR)
|| string_is_equal(entry.label, MENU_ENUM_LABEL_SAVE_STATE_STR))
{
char path[PATH_MAX_LENGTH];
runloop_state_t *runloop_st = runloop_state_get_ptr();
int state_slot = settings->ints.state_slot;
/* State slot dropdown */
if ( _state_slot == MENU_ENUM_LABEL_STATE_SLOT
|| string_is_equal(entry.label, MENU_ENUM_LABEL_STATE_SLOT_RUN_STR))
{
state_slot = i - 1;
xmb->is_state_slot = true;
}
gfx_savestate_thumbnail_get_path(path, sizeof(path),
runloop_st->name.savestate, state_slot);
strlcpy(xmb->savestate_thumbnail_file_path, path,
sizeof(xmb->savestate_thumbnail_file_path));
if (!string_is_equal(old_path, xmb->savestate_thumbnail_file_path))
gfx_thumbnail_reset(&xmb->thumbnails.savestate);
xmb->fullscreen_thumbnails_available = true;
menu_update_fullscreen_thumbnail_label(
xmb->fullscreen_thumbnail_label,
sizeof(xmb->fullscreen_thumbnail_label),
xmb->is_quick_menu,
NULL);
}
}
}
}
static void xmb_update_thumbnail_image(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
struct menu_state *menu_st = menu_state_get_ptr();
if (!xmb)
return;
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
gfx_thumbnail_cancel_pending_requests();
if (!xmb->skip_thumbnail_reset)
{
gfx_thumbnail_reset(&xmb->thumbnails.right);
gfx_thumbnail_reset(&xmb->thumbnails.left);
gfx_thumbnail_reset(&xmb->thumbnails.icon);
}
/* imageviewer content requires special treatment... */
if (
*menu_st->thumbnail_path_data->content_core_name
&& string_is_equal(menu_st->thumbnail_path_data->content_core_name,
"imageviewer"))
{
/* Right thumbnail */
if (gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT))
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_RIGHT;
/* Left thumbnail */
else if (gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT))
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_LEFT;
}
else
{
/* Right thumbnail */
if (gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT))
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_RIGHT;
/* Left thumbnail */
if (gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT))
xmb->thumbnails.pending =
(xmb->thumbnails.pending == XMB_PENDING_THUMBNAIL_RIGHT)
? XMB_PENDING_THUMBNAIL_BOTH : XMB_PENDING_THUMBNAIL_LEFT;
}
if (xmb->want_fullscreen_thumbnails)
menu_update_fullscreen_thumbnail_label(
xmb->fullscreen_thumbnail_label,
sizeof(xmb->fullscreen_thumbnail_label),
xmb->is_quick_menu ? true : false,
xmb->title_name);
}
static unsigned xmb_get_system_tab(xmb_handle_t *xmb, unsigned i)
{
if (i <= xmb->system_tab_end)
return xmb->tabs[i];
return UINT_MAX;
}
static unsigned xmb_get_horizontal_selection_type(xmb_handle_t *xmb)
{
if (xmb->categories_selection_ptr > xmb->system_tab_end)
{
size_t i = xmb->categories_selection_ptr - xmb->system_tab_end - 1;
return (xmb->horizontal_list.size) ? xmb->horizontal_list.list[i].type : 0;
}
return 0;
}
static void xmb_refresh_thumbnail_image(void *data, size_t i)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
struct menu_state *menu_st = menu_state_get_ptr();
if (!xmb)
return;
/* Only refresh thumbnails if thumbnails are enabled */
if ( (xmb->is_quick_menu || xmb->is_playlist || xmb->is_explore_list)
&& ( gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON))
)
xmb_update_thumbnail_image(xmb);
}
static void xmb_unload_thumbnail_textures(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (!xmb)
return;
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
gfx_thumbnail_cancel_pending_requests();
gfx_thumbnail_reset(&xmb->thumbnails.right);
gfx_thumbnail_reset(&xmb->thumbnails.left);
gfx_thumbnail_reset(&xmb->thumbnails.icon);
gfx_thumbnail_reset(&xmb->thumbnails.savestate);
}
static void xmb_unload_icon_thumbnail_textures(void *xmb_handle_ptr)
{
xmb_handle_t *xmb = (xmb_handle_t*)xmb_handle_ptr;
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
size_t list_size = (unsigned)selection_buf->size;
size_t i;
if (!xmb)
return;
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_NONE;
gfx_thumbnail_cancel_pending_requests();
for (i = 0; i < list_size; i++)
{
xmb_node_t *node = (xmb_node_t*)selection_buf->list[i].userdata;
if (node)
{
/* Resets the thumbnail and drops the whole ~15 KB icon block.
* The lazy resolver in xmb_render sees a NULL thumbnail_icon
* as "needs path resolution" and reallocates on the next
* pending_icons pass, so this is equivalent to the old
* "reset + clear icon_path[0]" invalidation -- we still don't
* eagerly re-resolve paths for every visible entry here -- and
* it additionally returns the memory for every entry that has
* scrolled out of relevance. */
xmb_node_icons_free(node);
}
}
}
static void xmb_set_thumbnail_content(void *data, const char *s)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
struct menu_state *menu_st = menu_state_get_ptr();
size_t selection = menu_st->selection_ptr;
if (!xmb)
return;
/* Disable fullscreen thumbnails by default,
* and only enable if thumbnail content is
* actually set
* > This is the easiest method for verifying
* that we are currently viewing a relevant
* menu type */
xmb->fullscreen_thumbnails_available = false;
if (xmb->is_playlist)
{
/* Playlist content */
if (!s || !*s)
{
menu_list_t *menu_list = menu_st->entries.list;
size_t list_size = (unsigned)MENU_LIST_GET_SELECTION(menu_list, 0)->size;
file_list_t *list = MENU_LIST_GET_SELECTION(menu_list, 0);
bool playlist_valid = false;
size_t playlist_index = selection;
/* Get playlist index corresponding
* to the selected entry */
if ( list
&& (selection < list_size)
&& (list->list[selection].type == FILE_TYPE_RPL_ENTRY))
{
playlist_valid = true;
playlist_index = list->list[selection].entry_idx;
}
gfx_thumbnail_set_content_playlist(menu_st->thumbnail_path_data,
playlist_valid ? playlist_get_cached() : NULL, playlist_index);
xmb->fullscreen_thumbnails_available = true;
}
}
else if (xmb->is_db_manager_list)
{
/* Database list content */
if (!s || !*s)
{
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
entry.flags |= MENU_ENTRY_FLAG_PATH_ENABLED;
menu_entry_get(&entry, 0, selection, NULL, true);
if (*entry.path)
{
gfx_thumbnail_set_content(menu_st->thumbnail_path_data, entry.path);
xmb->fullscreen_thumbnails_available = true;
}
}
}
#if defined(HAVE_LIBRETRODB)
else if (xmb->is_explore_list)
{
/* Explore list */
if (!s || !*s)
{
/* Selected entry */
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
entry.flags |= MENU_ENTRY_FLAG_PATH_ENABLED;
menu_entry_get(&entry, 0, selection, NULL, true);
xmb->fullscreen_thumbnails_available =
(menu_explore_set_playlist_thumbnail(entry.type, menu_st->thumbnail_path_data) >= 0);
}
}
#endif
else if (memcmp(s, "imageviewer", sizeof("imageviewer")) == 0)
{
/* Filebrowser image updates */
menu_entry_t entry;
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
xmb_node_t *node = (xmb_node_t*)selection_buf->list[selection].userdata;
if (node)
{
MENU_ENTRY_INITIALIZE(entry);
entry.flags |= MENU_ENTRY_FLAG_PATH_ENABLED;
menu_entry_get(&entry, 0, selection, NULL, true);
if ( *entry.path
&& (node->fullpath && *node->fullpath)
&& ( (entry.type == FILE_TYPE_IMAGEVIEWER)
|| (image_texture_get_type(entry.path) == IMAGE_TYPE_WEBM)
|| (image_texture_get_type(entry.path) == IMAGE_TYPE_MP4)))
{
gfx_thumbnail_set_content_image(menu_st->thumbnail_path_data,
node->fullpath, entry.path);
xmb->fullscreen_thumbnails_available = true;
}
}
}
else if (s && *s)
{
/* Annoying leftovers...
* This is required to ensure that thumbnails are
* updated correctly when navigating deeply through
* the sublevels of database manager lists.
* Showing thumbnails on database entries is a
* pointless nuisance and a waste of CPU cycles, IMHO... */
gfx_thumbnail_set_content(menu_st->thumbnail_path_data, s);
xmb->fullscreen_thumbnails_available = true;
}
}
static bool gfx_thumbnail_set_icon_playlist(
gfx_thumbnail_path_data_t *path_data, playlist_t *playlist, size_t idx)
{
const char *content_path = NULL;
const char *content_label = NULL;
const char *core_name = NULL;
const char *db_name = NULL;
const struct playlist_entry *entry = NULL;
if (!path_data)
return false;
/* When content is updated, must regenerate icon
* thumbnail paths */
path_data->icon_path[0] = '\0';
/* 'Reset' path_data content strings */
path_data->content_path[0] = '\0';
path_data->content_label_len = 0;
path_data->content_label[0] = '\0';
path_data->content_core_name[0] = '\0';
path_data->content_db_name[0] = '\0';
path_data->content_img[0] = '\0';
path_data->content_img_full[0] = '\0';
path_data->content_img_short[0] = '\0';
/* Must also reset playlist thumbnail display modes */
path_data->playlist_icon_mode = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
path_data->playlist_index = 0;
if (!playlist)
return false;
if (idx >= playlist_get_size(playlist))
return false;
/* Read playlist values */
playlist_get_index(playlist, idx, &entry);
if (!entry)
return false;
content_path = entry->path;
content_label = entry->label;
core_name = entry->core_name;
db_name = entry->db_name;
/* Content without a path is invalid by definition */
if (!content_path || !*content_path)
return false;
/* Cache content path
* (This is required for imageviewer, history and favourites content) */
strlcpy(path_data->content_path,
content_path, sizeof(path_data->content_path));
/* Cache core name
* (This is required for imageviewer content) */
if (core_name && *core_name)
strlcpy(path_data->content_core_name,
core_name, sizeof(path_data->content_core_name));
/* Get content label */
if (content_label && *content_label)
path_data->content_label_len = strlcpy(path_data->content_label,
content_label, sizeof(path_data->content_label));
else
path_data->content_label_len = fill_pathname(
path_data->content_label,
path_basename(content_path),
"", sizeof(path_data->content_label));
/* Determine content image name */
{
char tmp_buf[NAME_MAX_LENGTH];
fill_pathname(tmp_buf, path_basename(path_data->content_path), "",
sizeof(tmp_buf));
gfx_thumbnail_fill_content_img(path_data->content_img_full,
sizeof(path_data->content_img_full), tmp_buf, false);
gfx_thumbnail_fill_content_img(path_data->content_img,
sizeof(path_data->content_img), path_data->content_label, false);
/* Explicit zero if full name is same as standard name - saves some queries later. */
if (string_is_equal(path_data->content_img, path_data->content_img_full))
path_data->content_img_full[0] = '\0';
gfx_thumbnail_fill_content_img(path_data->content_img_short,
sizeof(path_data->content_img_short), path_data->content_label, true);
}
/* Store playlist index */
path_data->playlist_index = idx;
/* Redundant error check... */
if (!*path_data->content_img)
return false;
/* Thumbnail image name is done -> now check if
* per-content database name is defined */
if (!db_name || !*db_name)
playlist_get_db_name(playlist, idx, &db_name);
if (db_name && *db_name)
{
/* Hack: There is only one MAME thumbnail repo,
* so filter any input starting with 'MAME...' */
if (strncmp(db_name, "MAME", 4) == 0)
{
path_data->content_db_name[0] = path_data->content_db_name[2] = 'M';
path_data->content_db_name[1] = 'A';
path_data->content_db_name[3] = 'E';
path_data->content_db_name[4] = '\0';
}
else
{
size_t i;
char tmp_buf[NAME_MAX_LENGTH];
const size_t __len = sizeof(tmp_buf) - 1;
for (i = 0; i < __len && db_name[i] && db_name[i] != '|'; i++)
tmp_buf[i] = db_name[i];
tmp_buf[i] = '\0';
fill_pathname(path_data->content_db_name,
tmp_buf, "", sizeof(path_data->content_db_name));
}
}
gfx_thumbnail_update_path(path_data, GFX_THUMBNAIL_ICON);
/* Playlist entry is valid -> it is now 'safe' to
* extract any remaining playlist metadata
* (i.e. thumbnail display modes) */
path_data->playlist_icon_mode = PLAYLIST_THUMBNAIL_MODE_DEFAULT;
return true;
}
static void xmb_set_dynamic_icon_content(
void *xmb_handle_ptr,
const char *s,
size_t selection,
xmb_icons_t *thumbnail_icon)
{
xmb_handle_t *xmb = (xmb_handle_t*)xmb_handle_ptr;
if (!xmb || !xmb->is_playlist)
return;
/* Playlist content */
if (!s || !*s)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
size_t list_size = (unsigned)MENU_LIST_GET_SELECTION(menu_list, 0)->size;
file_list_t *list = MENU_LIST_GET_SELECTION(menu_list, 0);
bool playlist_valid = false;
size_t playlist_index = selection;
/* Get playlist index corresponding
* to the selected entry */
if ( list
&& (selection < list_size)
&& (list->list[selection].type == FILE_TYPE_RPL_ENTRY))
{
playlist_valid = true;
playlist_index = list->list[selection].entry_idx;
}
gfx_thumbnail_set_icon_playlist(&thumbnail_icon->thumbnail_path_data,
playlist_valid ? playlist_get_cached() : NULL, playlist_index);
}
}
static void xmb_update_savestate_thumbnail_image(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
unsigned upscale_threshold = config_get_ptr()->uints.gfx_thumbnail_upscale_threshold;
if (!xmb || xmb->skip_thumbnail_reset)
return;
/* If path is empty, just reset thumbnail */
if (!*xmb->savestate_thumbnail_file_path)
gfx_thumbnail_reset(&xmb->thumbnails.savestate);
else if (xmb->thumbnails.savestate.status == GFX_THUMBNAIL_STATUS_UNKNOWN)
gfx_thumbnail_request_file(
xmb->savestate_thumbnail_file_path,
&xmb->thumbnails.savestate,
upscale_threshold);
xmb->thumbnails.savestate.flags |= GFX_THUMB_FLAG_CORE_ASPECT | GFX_THUMB_FLAG_BG_ONLY;
}
/* Is called when the pointer position changes
* within a list/sub-list (vertically) */
static void xmb_selection_pointer_changed(
xmb_handle_t *xmb, bool allow_animations)
{
unsigned i, end, height;
size_t num = 0;
int threshold = 0;
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
uintptr_t tag = (uintptr_t)selection_buf;
size_t selection = menu_st->selection_ptr;
unsigned anim_move_up_down = config_get_ptr()->uints.menu_xmb_animation_move_up_down;
if (anim_move_up_down > 1)
allow_animations = false;
if (!xmb)
return;
end = (unsigned)MENU_LIST_GET_SELECTION(menu_list, 0)->size;
threshold = xmb->icon_size * 10;
menu_st->entries.begin = num;
height = xmb->last_height;
/* On cursor movement within a playlist, invalidate any in-flight
* icon thumbnail requests (they're for the previous cursor position
* and no longer relevant) and raise pending_icons so the render
* dispatcher will lazily resolve paths + load textures for the new
* visible range under its per-frame cap. We deliberately do NOT
* eagerly pre-resolve paths for all visible entries here — the
* selection pointer can move many times per second during held-down
* scrolling, and re-resolving up to ~20 paths per step (including
* path_is_valid syscalls, amplified up to ×7 by
* playlist_allow_non_png)
* is enough to blow frame budgets. Lazy resolution in the
* dispatcher gets the same visible result for a fraction of the
* work. The is_playlist / title-name gates match the ones the old
* xmb_refresh_visible_icon_paths helper used internally. */
if ( xmb->is_playlist
&& gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON)
&& !string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_IMAGES_TAB))
&& !string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MUSIC_TAB))
&& !string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_TAB)))
{
gfx_thumbnail_cancel_pending_requests();
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
}
/* Selection-only work, hoisted out of the loop below.
*
* This all used to sit inside the per-entry loop under an
* `if (i == selection)` guard, so its whole body -- a menu_entry_t,
* the thumbnail-content dispatch and the savestate path buffers --
* was inlined into a loop that runs once per list entry. On a MAME
* or FBNeo playlist that loop is tens of thousands of iterations and
* it runs on every cursor movement, so the register pressure the
* block created was being paid per entry per keypress.
*
* Ordering: this now runs before the loop rather than partway
* through it. Nothing here reads or writes the node animation
* fields the loop sets, and the thumbnail animations it can kill are
* tagged by thumbnail address, never by selection_buf, so they
* cannot collide with the tag the loop pushes under. */
if ( selection < end
&& selection_buf->list[selection].userdata)
{
unsigned depth = (unsigned)xmb_list_get_size(xmb, MENU_LIST_PLAIN);
/* Update entry index text */
if (xmb->entry_idx_enabled)
{
size_t entry_idx_selection = selection + 1;
size_t list_size = MENU_LIST_GET_SELECTION(menu_list, 0)->size;
unsigned entry_idx_offset = xmb->entry_index_offset;
bool show_entry_idx = (xmb->is_playlist || xmb->is_explore_list) ? true : false;
if (xmb->is_explore_list)
{
if (entry_idx_selection > entry_idx_offset)
entry_idx_selection -= entry_idx_offset;
else
show_entry_idx = false;
}
if (!show_entry_idx)
xmb->entry_index_str[0] = '\0';
else
snprintf(xmb->entry_index_str, sizeof(xmb->entry_index_str),
"%lu/%lu", (unsigned long)entry_idx_selection,
(unsigned long)list_size);
}
if ( gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON))
{
bool update_thumbnails = false;
/* Playlist updates */
if (xmb->is_playlist)
{
xmb_set_thumbnail_content(xmb, NULL);
update_thumbnails = true;
}
/* Database + Explore list updates */
else if ((xmb->is_db_manager_list && depth <= 4)
|| xmb->is_explore_list)
{
xmb_set_thumbnail_content(xmb, NULL);
update_thumbnails = true;
xmb->skip_thumbnail_reset = false;
}
else if (xmb->is_db_manager_list && depth == 5)
xmb->skip_thumbnail_reset = true;
/* Filebrowser image updates */
else if (xmb->is_file_list)
{
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
entry.flags |= MENU_ENTRY_FLAG_PATH_ENABLED;
menu_entry_get(&entry, 0, selection, NULL, true);
if ( (entry.type == FILE_TYPE_IMAGEVIEWER)
|| (entry.type == FILE_TYPE_IMAGE)
/* WebM and MP4 files preview like images: the thumbnail
* pipeline decodes their video track */
|| (image_texture_get_type(entry.path) == IMAGE_TYPE_WEBM)
|| (image_texture_get_type(entry.path) == IMAGE_TYPE_MP4))
{
xmb_set_thumbnail_content(xmb, "imageviewer");
update_thumbnails = true;
}
else
{
/* If this is a file list and current
* entry is not an image, have to 'reset'
* content + right/left thumbnails
* (otherwise last loaded thumbnail will
* persist, and be shown on the wrong entry) */
xmb->fullscreen_thumbnails_available = false;
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
gfx_thumbnail_set_content(menu_st->thumbnail_path_data, NULL);
gfx_thumbnail_cancel_pending_requests();
gfx_thumbnail_reset(&xmb->thumbnails.right);
gfx_thumbnail_reset(&xmb->thumbnails.left);
gfx_thumbnail_reset(&xmb->thumbnails.icon);
}
}
if (update_thumbnails && end > 1)
xmb_update_thumbnail_image(xmb);
}
if (end > 1)
{
xmb_update_savestate_thumbnail_path(xmb, selection);
xmb_update_savestate_thumbnail_image(xmb);
}
}
for (i = 0; i < end; i++)
{
float iy, real_iy;
float ia;
float iz;
xmb_node_t *node = (xmb_node_t*)selection_buf->list[i].userdata;
if (!node)
continue;
if (i == selection)
{
ia = xmb->items_active_alpha;
iz = xmb->items_active_zoom;
}
else
{
ia = xmb->items_passive_alpha;
iz = xmb->items_passive_zoom;
}
iy = xmb_item_y(xmb, i, selection);
real_iy = iy + xmb->margins_screen_top;
if ( !allow_animations
|| real_iy < -threshold
|| real_iy > height + threshold)
{
node->alpha = node->label_alpha = ia;
node->y = iy;
node->zoom = iz;
}
else
{
/* Move up/down animation */
gfx_animation_ctx_entry_t anim_entry;
anim_entry.target_value = ia;
anim_entry.subject = &node->alpha;
anim_entry.tag = tag;
anim_entry.cb = NULL;
switch (anim_move_up_down)
{
case 0:
anim_entry.duration = XMB_DELAY;
anim_entry.easing_enum = EASING_OUT_QUAD;
break;
case 1:
anim_entry.duration = XMB_DELAY * 4;
anim_entry.easing_enum = EASING_OUT_EXPO;
break;
}
gfx_animation_push(&anim_entry);
anim_entry.subject = &node->label_alpha;
gfx_animation_push(&anim_entry);
anim_entry.target_value = iz;
anim_entry.subject = &node->zoom;
gfx_animation_push(&anim_entry);
anim_entry.target_value = iy;
anim_entry.subject = &node->y;
gfx_animation_push(&anim_entry);
}
}
}
static void xmb_list_open_new(xmb_handle_t *xmb,
bool horizontal_animation,
bool savestate_thumbnail,
file_list_t *list, int dir, size_t current)
{
unsigned i;
unsigned height = xmb->last_height;
size_t skip = 0;
int threshold = xmb->icon_size * 10;
size_t end = list ? list->size : 0;
struct menu_state *menu_st = menu_state_get_ptr();
for (i = 0; i < end; i++)
{
float ia;
float real_y;
xmb_node_t *node = (xmb_node_t*)list->list[i].userdata;
if (!node)
continue;
if (dir == 1)
{
node->alpha = 0;
node->label_alpha = 0;
}
else if (dir == -1)
{
if (i != current)
node->alpha = 0;
node->label_alpha = 0;
}
node->x = xmb->icon_size * dir * 2;
node->y = xmb_item_y(xmb, i, current);
node->zoom = xmb->categories_passive_zoom;
real_y = node->y + xmb->margins_screen_top;
if (i == current)
{
node->zoom = xmb->categories_active_zoom;
ia = xmb->items_active_alpha;
}
else
ia = xmb->items_passive_alpha;
if ( !horizontal_animation
|| real_y < -threshold
|| real_y > height + threshold)
{
node->alpha = node->label_alpha = ia;
node->x = 0;
}
else
{
gfx_animation_ctx_entry_t anim_entry;
node->alpha /= 5;
anim_entry.duration = XMB_DELAY;
anim_entry.target_value = ia;
anim_entry.subject = &node->alpha;
anim_entry.easing_enum = XMB_EASING_ALPHA;
anim_entry.tag = (uintptr_t)list;
anim_entry.cb = NULL;
gfx_animation_push(&anim_entry);
anim_entry.subject = &node->label_alpha;
gfx_animation_push(&anim_entry);
anim_entry.target_value = 0;
anim_entry.subject = &node->x;
anim_entry.easing_enum = XMB_EASING_XY;
gfx_animation_push(&anim_entry);
}
}
menu_st->entries.begin = skip;
if ( gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON))
{
if (xmb->is_playlist || xmb->is_db_manager_list || xmb->is_explore_list)
{
if ( !(xmb->is_db_manager_list && xmb->depth > 3)
&& !xmb->skip_thumbnail_reset)
xmb_unload_thumbnail_textures(xmb);
xmb_set_thumbnail_content(xmb, NULL);
xmb_update_thumbnail_image(xmb);
if (!xmb->is_explore_list)
xmb->skip_thumbnail_reset = false;
}
}
if ( savestate_thumbnail
&& ((xmb->is_quick_menu && xmb->depth >= 2)
|| (xmb->is_state_slot)))
{
/* This shows savestate thumbnail after
* opening savestate submenu */
xmb->skip_thumbnail_reset = false;
xmb_update_savestate_thumbnail_path(xmb, (unsigned)current);
xmb_update_savestate_thumbnail_image(xmb);
}
xmb->old_depth = xmb->depth;
}
static xmb_node_t *xmb_node_allocate_userdata(
xmb_handle_t *xmb, unsigned i)
{
xmb_node_t *tmp = NULL;
xmb_node_t *node = xmb_alloc_node();
if (!node)
return NULL;
node->alpha = xmb->categories_passive_alpha;
node->zoom = xmb->categories_passive_zoom;
if ((i + xmb->system_tab_end) == xmb->categories_active_idx)
{
node->alpha = xmb->categories_active_alpha;
node->zoom = xmb->categories_active_zoom;
}
tmp = (xmb_node_t*)file_list_get_userdata_at_offset(&xmb->horizontal_list, i);
if (tmp)
{
/* Kill in-flight highlight tweens on the node before freeing it;
* they are tagged by the horizontal list (see below). */
uintptr_t tag = (uintptr_t)&xmb->horizontal_list;
gfx_animation_kill_by_tag(&tag);
}
xmb_free_node(tmp);
xmb->horizontal_list.userdata_free = xmb_free_node_cb;
xmb->horizontal_list.list[i].userdata = node;
return node;
}
static xmb_node_t* xmb_get_userdata_from_horizontal_list(
xmb_handle_t *xmb, unsigned i)
{
return (xmb_node_t*)file_list_get_userdata_at_offset(&xmb->horizontal_list, i);
}
static void xmb_push_animations(xmb_node_t *node,
uintptr_t tag, float ia, float ix)
{
gfx_animation_ctx_entry_t anim_entry;
node->alpha /= 5;
anim_entry.duration = XMB_DELAY;
anim_entry.target_value = ia;
anim_entry.subject = &node->alpha;
anim_entry.easing_enum = XMB_EASING_ALPHA;
anim_entry.tag = tag;
anim_entry.cb = NULL;
gfx_animation_push(&anim_entry);
anim_entry.subject = &node->label_alpha;
gfx_animation_push(&anim_entry);
anim_entry.target_value = ix;
anim_entry.subject = &node->x;
anim_entry.easing_enum = XMB_EASING_XY;
gfx_animation_push(&anim_entry);
}
XMB_NOINLINE static void xmb_animation_list_alpha(xmb_handle_t *xmb, bool fade_in)
{
struct gfx_animation_ctx_entry entry;
entry.cb = NULL;
entry.duration = XMB_DELAY * 4;
entry.easing_enum = XMB_EASING_ALPHA;
entry.subject = &xmb->alpha_list;
entry.tag = (uintptr_t)NULL;
entry.target_value = (fade_in) ? 1.0f : 0.0f;
entry.userdata = xmb;
gfx_animation_push(&entry);
}
static void xmb_list_switch_new(xmb_handle_t *xmb,
file_list_t *list, int dir, size_t current)
{
unsigned i;
unsigned height = xmb->last_height;
unsigned last = 0;
unsigned first = 0;
size_t end = 0;
bool menu_hor_anim = config_get_ptr()->bools.menu_horizontal_animation;
if (list)
end = list->size;
if (end > 0)
last = (unsigned)(end - 1);
xmb_calculate_visible_range(xmb, height, end, (unsigned)current, &first, &last);
for (i = 0; i < end; i++)
{
xmb_node_t *node = (xmb_node_t*)list->list[i].userdata;
float ia = xmb->items_passive_alpha;
if (!node)
continue;
node->x = xmb->icon_spacing_horizontal * dir;
node->alpha = 0;
node->label_alpha = 0;
if (i == current)
ia = xmb->items_active_alpha;
if (i >= first && i <= last)
{
if (!menu_hor_anim)
{
node->alpha = node->label_alpha = ia;
node->x = 0;
}
else
xmb_push_animations(node, (uintptr_t)list, ia, 0);
}
else
{
node->x = 0;
node->alpha = node->label_alpha = ia;
}
}
}
static enum msg_hash_enums xmb_search_enum(const char *label)
{
unsigned i;
enum msg_hash_enums enum_idx = MSG_UNKNOWN;
for (i = 0; i < MSG_LAST && enum_idx == MSG_UNKNOWN; i++)
{
if (string_is_equal(label, msg_hash_to_str((enum msg_hash_enums)i)))
enum_idx = (enum msg_hash_enums)i;
}
return enum_idx;
}
static void xmb_set_title(xmb_handle_t *xmb)
{
char *scrub_char_ptr = NULL;
xmb->title_name_alt[0] = '\0';
if ( (xmb->categories_selection_ptr <= xmb->system_tab_end)
|| (xmb->is_quick_menu && !menu_is_running_quick_menu())
|| (xmb->depth > 1))
{
menu_entries_get_title(xmb->title_name, sizeof(xmb->title_name));
strlcpy(xmb->title_name_alt, xmb->title_name, sizeof(xmb->title_name_alt));
}
else
{
xmb_node_t *node = (xmb_node_t*)file_list_get_userdata_at_offset(
&xmb->horizontal_list,
xmb->categories_selection_ptr - (xmb->system_tab_end + 1));
const char *path = xmb->horizontal_list.list[
xmb->categories_selection_ptr - (xmb->system_tab_end + 1)].path;
if (!path)
return;
/* Use real title for dynamic backgrounds */
fill_pathname(xmb->title_name, path_basename(path), "", sizeof(xmb->title_name));
/* Set alternative title for visible header */
if (node && node->console_name)
{
strlcpy(xmb->title_name_alt,
node->console_name,
sizeof(xmb->title_name_alt));
/* Add current search terms to alternative title */
menu_entries_search_append_terms_string(
xmb->title_name_alt, sizeof(xmb->title_name_alt));
}
}
while ((scrub_char_ptr = strchr(xmb->title_name, '/')))
*scrub_char_ptr = '-';
while ((scrub_char_ptr = strchr(xmb->title_name, ':')))
*scrub_char_ptr = '_';
if (*xmb->title_name)
{
int sub = string_find_index_substring_string(xmb->title_name, " (");
if (sub > 0)
xmb->title_name[sub] = '\0';
}
if ( !xmb->assets_missing
&& config_get_ptr()->uints.menu_xmb_current_menu_icon)
{
char label_temp[NAME_MAX_LENGTH];
unsigned type = FILE_TYPE_NONE;
size_t entry_idx = 0;
enum msg_hash_enums enum_idx = MSG_UNKNOWN;
const char *path = NULL;
const char *label = NULL;
const char *label_original = NULL;
uintptr_t texture = xmb->textures.list[XMB_TEXTURE_QUICKMENU];
bool search = true;
/* Preserve and decrement any in-flight retry countdown from
* xmb_render(). Fresh fallback (prev_retry == 0) (re)arms to
* ~1s @ 60fps; ongoing retry counts down one frame at a time
* so a permanently-missing asset can't spin forever. Cleared
* unconditionally first — any fallback branch below will set
* the next value. */
uint8_t prev_retry = xmb->current_menu_icon_retry;
xmb->current_menu_icon_retry = 0;
menu_entries_get_last_stack(&path, &label, &type, &enum_idx, &entry_idx);
label_original = label;
/* Direct exceptions */
if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_CORE_SYSTEM_FILES_LIST_STR))
enum_idx = MENU_ENUM_LABEL_DOWNLOAD_CORE_SYSTEM_FILES;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_CORE_CONTENT_DIRS_LIST_STR))
enum_idx = MENU_ENUM_LABEL_DOWNLOAD_CORE_CONTENT_DIRS;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_CORE_LIST_SET_STR))
enum_idx = MENU_ENUM_LABEL_CORE_LIST;
else if (string_is_equal(label, MENU_ENUM_LABEL_MAIN_MENU_STR))
enum_idx = MENU_ENUM_LABEL_MAIN_MENU;
else if (string_is_equal(label, MENU_ENUM_LABEL_SETTINGS_TAB_STR))
enum_idx = MENU_ENUM_LABEL_SETTINGS_TAB;
else if (string_is_equal(label, MENU_ENUM_LABEL_HISTORY_TAB_STR))
enum_idx = MENU_ENUM_LABEL_LOAD_CONTENT_HISTORY;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_FAVORITES_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_FAVORITES_TAB_STR))
enum_idx = MENU_ENUM_LABEL_GOTO_FAVORITES;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_IMAGES_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_IMAGES_TAB_STR))
enum_idx = MENU_ENUM_LABEL_GOTO_IMAGES;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_MUSIC_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_MUSIC_TAB_STR))
enum_idx = MENU_ENUM_LABEL_GOTO_MUSIC;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_VIDEO_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_VIDEO_TAB_STR))
enum_idx = MENU_ENUM_LABEL_GOTO_VIDEO;
else if (string_is_equal(label, MENU_ENUM_LABEL_ADD_TAB_STR))
enum_idx = MENU_ENUM_LABEL_ADD_CONTENT_LIST;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_CONTENTLESS_CORES_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_CONTENTLESS_CORES_TAB_STR))
enum_idx = MENU_ENUM_LABEL_GOTO_CONTENTLESS_CORES;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_EXPLORE_LIST_STR))
enum_idx = MENU_ENUM_LABEL_DEFERRED_EXPLORE_LIST;
else if (string_is_equal(label, MENU_ENUM_LABEL_EXPLORE_TAB_STR))
enum_idx = MENU_ENUM_LABEL_EXPLORE_TAB;
else if (string_is_equal(label, MENU_ENUM_LABEL_HORIZONTAL_MENU_STR))
enum_idx = MENU_ENUM_LABEL_HORIZONTAL_MENU;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_RPL_ENTRY_ACTIONS_STR))
enum_idx = MENU_ENUM_LABEL_DEFERRED_RPL_ENTRY_ACTIONS;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_USER_BINDS_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_REMAPPINGS_PORT_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_DEVICE_TYPE_STR))
enum_idx = MENU_ENUM_LABEL_INPUT_DEVICE_INDEX;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_SELECT_RESERVED_DEVICE_STR))
enum_idx = MENU_ENUM_LABEL_INPUT_DEVICE_RESERVED_DEVICE_NAME;
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_DESCRIPTION_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_DESCRIPTION_KBD_STR))
{
unsigned input_id;
if (type < MENU_SETTINGS_INPUT_DESC_KBD_BEGIN)
input_id = MENU_SETTINGS_INPUT_DESC_BEGIN;
else
input_id = MENU_SETTINGS_INPUT_DESC_KBD_BEGIN;
while (type > (input_id + (RARCH_ANALOG_BIND_LIST_END - 1)))
input_id = (input_id + RARCH_ANALOG_BIND_LIST_END);
type = type - input_id;
enum_idx = MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_DESCRIPTION;
}
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_RETROPAD_BIND_STR))
enum_idx = (enum msg_hash_enums)atoi(path);
else if (string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_STR))
{
enum_idx = (enum msg_hash_enums)atoi(path);
if ( enum_idx >= MENU_ENUM_LABEL_INPUT_DEVICE_INDEX
&& enum_idx <= MENU_ENUM_LABEL_INPUT_DEVICE_INDEX_LAST)
enum_idx = MENU_ENUM_LABEL_INPUT_DEVICE_INDEX;
else if (enum_idx >= MENU_ENUM_LABEL_INPUT_MOUSE_INDEX
&& enum_idx <= MENU_ENUM_LABEL_INPUT_MOUSE_INDEX_LAST)
enum_idx = MENU_ENUM_LABEL_INPUT_MOUSE_INDEX;
else if (enum_idx >= MENU_ENUM_LABEL_INPUT_PLAYER_ANALOG_DPAD_MODE
&& enum_idx <= MENU_ENUM_LABEL_INPUT_PLAYER_ANALOG_DPAD_MODE_LAST)
enum_idx = MENU_ENUM_LABEL_INPUT_PLAYER_ANALOG_DPAD_MODE;
else if (enum_idx >= MENU_ENUM_LABEL_INPUT_REMAP_PORT
&& enum_idx <= MENU_ENUM_LABEL_INPUT_REMAP_PORT_LAST)
enum_idx = MENU_ENUM_LABEL_INPUT_REMAP_PORT;
else if (enum_idx >= MENU_ENUM_LABEL_INPUT_DEVICE_RESERVATION_TYPE
&& enum_idx <= MENU_ENUM_LABEL_INPUT_DEVICE_RESERVATION_TYPE_LAST)
enum_idx = MENU_ENUM_LABEL_INPUT_DEVICE_RESERVATION_TYPE;
}
if (type == MENU_SETTING_ACTION_CORE_OPTIONS)
path = xmb->title_name;
else if (string_ends_with(path, FILE_PATH_RDB_EXTENSION))
type = FILE_TYPE_RDB_ENTRY;
if (type == FILE_TYPE_RDB_ENTRY || enum_idx != MSG_UNKNOWN)
search = false;
if (search)
{
/* Due to inconsistent standards in 'msg_hash_lbl.h'
* and in order to avoid adding each and every
* deferred enum variant in 'xmb_icon_get_id()'
* search must be done in separate segments first
* removing prefix and then suffix if warranted. */
if (string_starts_with(label, "deferred_"))
{
strlcpy(label_temp, label + STRLEN_CONST("deferred_"), sizeof(label_temp));
label = label_temp;
}
if (enum_idx == MSG_UNKNOWN)
enum_idx = xmb_search_enum(label);
if (enum_idx == MSG_UNKNOWN)
{
if (string_ends_with(label, "_list"))
{
label_temp[strlen(label_temp) - STRLEN_CONST("_list")] = '\0';
label = label_temp;
enum_idx = xmb_search_enum(label);
}
else if (string_starts_with(label, "dropdown_box_list_"))
{
label = label_original;
enum_idx = xmb_search_enum(label);
}
}
}
switch (enum_idx)
{
default:
break;
case MENU_ENUM_LABEL_DEFERRED_RPL_ENTRY_ACTIONS:
case MENU_ENUM_LABEL_HORIZONTAL_MENU:
{
const struct playlist_entry *pl_entry = NULL;
xmb_node_t *db_node = NULL;
/* Horizontal menu */
if (xmb->categories_selection_ptr > xmb->system_tab_end)
{
xmb_node_t *sidebar_node = NULL;
size_t i = xmb->categories_selection_ptr - xmb->system_tab_end - 1;
/* Explore views */
if (string_ends_with_size(xmb->horizontal_list.list[i].label, ".lvw",
strlen(xmb->horizontal_list.list[i].label), STRLEN_CONST(".lvw")))
{
texture = xmb->textures.list[XMB_TEXTURE_CURSOR];
goto end;
}
/* Playlists */
sidebar_node = (xmb_node_t*)file_list_get_userdata_at_offset(&xmb->horizontal_list, i);
if (sidebar_node && sidebar_node->icon)
texture = sidebar_node->icon;
else if (sidebar_node)
/* Async load still in flight (e.g. right after a
* fullscreen toggle triggered xmb_context_reset) —
* ask xmb_render() to retry. Fresh trigger arms to
* 60 frames (~1s); ongoing retry decrements one
* step so a missing asset terminates the loop. */
xmb->current_menu_icon_retry = prev_retry ? prev_retry - 1 : 60;
}
/* Playlists entries */
entry_idx = menu_state_get_ptr()->thumbnail_path_data->playlist_index;
playlist_get_index(playlist_get_cached(), entry_idx, &pl_entry);
if ( pl_entry
&& (pl_entry->db_name && *pl_entry->db_name)
&& (db_node = RHMAP_GET_STR(xmb->playlist_db_node_map, pl_entry->db_name)))
{
/* Sidebar / content icons load asynchronously via
* xmb_context_reset_horizontal_list(). A context reset
* (e.g. fullscreen toggle) will zero these handles and
* re-queue the loads; xmb_set_title() runs before the
* tasks complete. Fall back to the existing `texture`
* (XMB_TEXTURE_QUICKMENU by default, or sidebar_node->icon
* if that happened to be set above) rather than assigning
* a 0 handle — which would blank the icon until the next
* navigation refresh. Flag pending so xmb_render() retries. */
uintptr_t db_icon = (enum_idx == MENU_ENUM_LABEL_HORIZONTAL_MENU)
? db_node->icon : db_node->content_icon;
if (db_icon)
texture = db_icon;
else
xmb->current_menu_icon_retry = prev_retry ? prev_retry - 1 : 60;
}
else
{
const playlist_config_t *pl_config = playlist_get_config(playlist_get_cached());
/* playlist_get_cached() is NULL while a deferred
* cached init is part way through its parse. Keep
* the default texture for this frame and arm the
* retry, like the async sidebar/db icon loads above -
* xmb_render() calls back in and the icon resolves
* once the parse lands. */
if (!pl_config)
xmb->current_menu_icon_retry = prev_retry ? prev_retry - 1 : 60;
else if (string_ends_with(pl_config->path, FILE_PATH_CONTENT_IMAGE_HISTORY))
texture = xmb->textures.list[XMB_TEXTURE_IMAGE];
else if (string_ends_with(pl_config->path, FILE_PATH_CONTENT_MUSIC_HISTORY))
texture = xmb->textures.list[XMB_TEXTURE_MUSIC];
else if (string_ends_with(pl_config->path, FILE_PATH_CONTENT_VIDEO_HISTORY))
texture = xmb->textures.list[XMB_TEXTURE_MOVIE];
else if (xmb->categories_selection_ptr < xmb->system_tab_end)
texture = xmb->textures.list[XMB_TEXTURE_FILE];
}
goto end;
break;
}
#ifdef HAVE_LIBRETRODB
case MENU_ENUM_LABEL_EXPLORE_TAB:
if (xmb_is_main_menu_explore() || !menu_explore_is_content_list())
enum_idx = MENU_ENUM_LABEL_GOTO_EXPLORE;
else
enum_idx = MENU_ENUM_LABEL_EXPLORE_ITEM;
break;
case MENU_ENUM_LABEL_DEFERRED_EXPLORE_LIST:
if (!string_is_equal(path, MENU_ENUM_LABEL_GOTO_EXPLORE_STR))
enum_idx = MENU_ENUM_LABEL_EXPLORE_ITEM;
else
enum_idx = MENU_ENUM_LABEL_GOTO_EXPLORE;
break;
#endif
}
texture = xmb_icon_get_id(xmb, NULL, NULL, enum_idx, path, label, type, 0, false);
end:
xmb->current_menu_icon = texture;
}
}
static xmb_node_t* xmb_get_node(xmb_handle_t *xmb, unsigned i)
{
switch (xmb_get_system_tab(xmb, i))
{
case XMB_SYSTEM_TAB_SETTINGS:
return &xmb->settings_tab_node;
#ifdef HAVE_IMAGEVIEWER
case XMB_SYSTEM_TAB_IMAGES:
return &xmb->images_tab_node;
#endif
case XMB_SYSTEM_TAB_MUSIC:
return &xmb->music_tab_node;
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
case XMB_SYSTEM_TAB_VIDEO:
return &xmb->video_tab_node;
#endif
case XMB_SYSTEM_TAB_HISTORY:
return &xmb->history_tab_node;
case XMB_SYSTEM_TAB_FAVORITES:
return &xmb->favorites_tab_node;
#ifdef HAVE_NETWORKING
case XMB_SYSTEM_TAB_NETPLAY:
return &xmb->netplay_tab_node;
#endif
case XMB_SYSTEM_TAB_ADD:
return &xmb->add_tab_node;
#if defined(HAVE_LIBRETRODB)
case XMB_SYSTEM_TAB_EXPLORE:
return &xmb->explore_tab_node;
#endif
case XMB_SYSTEM_TAB_CONTENTLESS_CORES:
return &xmb->contentless_cores_tab_node;
default:
if (i > xmb->system_tab_end)
return xmb_get_userdata_from_horizontal_list(
xmb, i - (xmb->system_tab_end + 1));
}
return &xmb->main_menu_node;
}
static void xmb_list_switch_horizontal_list(xmb_handle_t *xmb,
bool horizontal_animation,
unsigned animation_horizontal_highlight)
{
unsigned j;
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL)
+ xmb->system_tab_end;
for (j = 0; j <= list_size; j++)
{
gfx_animation_ctx_entry_t entry;
float ia = xmb->categories_passive_alpha;
float iz = xmb->categories_passive_zoom;
xmb_node_t *node = xmb_get_node(xmb, j);
if (!node)
continue;
if (j == xmb->categories_active_idx)
{
ia = xmb->categories_active_alpha;
iz = xmb->categories_active_zoom;
}
if ( !xmb->allow_horizontal_animation
|| !horizontal_animation)
{
node->alpha = ia;
node->zoom = iz;
continue;
}
/* Horizontal icon highlight animation */
entry.target_value = ia;
entry.subject = &node->alpha;
/* Playlist categories (j > system_tab_end) are heap nodes in
* horizontal_list that can be freed at runtime (e.g. a dbscan
* finishing); tag their tweens with the owning list so the free
* paths can kill them. Embedded system-tab nodes are never freed
* and keep the untagged (-1) sentinel. */
entry.tag = (j > xmb->system_tab_end)
? (uintptr_t)&xmb->horizontal_list : (uintptr_t)-1;
entry.cb = NULL;
switch (animation_horizontal_highlight)
{
case 0:
entry.duration = XMB_DELAY;
entry.easing_enum = EASING_OUT_QUAD;
break;
case 1:
entry.duration = XMB_DELAY + (XMB_DELAY / 2);
entry.easing_enum = EASING_IN_SINE;
break;
case 2:
entry.duration = XMB_DELAY * 2;
entry.easing_enum = EASING_OUT_BOUNCE;
break;
}
gfx_animation_push(&entry);
entry.target_value = iz;
entry.subject = &node->zoom;
gfx_animation_push(&entry);
}
}
static void xmb_tab_set_selection(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (xmb)
{
size_t tab_selection = xmb->tab_selection[xmb->categories_selection_ptr];
if (tab_selection)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_st->selection_ptr = tab_selection;
xmb_selection_pointer_changed(xmb, false);
}
}
}
static void xmb_populate_dynamic_icons(xmb_handle_t *xmb)
{
struct menu_state *menu_st = menu_state_get_ptr();
if (!gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON))
return;
/* Used when the displayed list changes (new playlist pushed or
* context reset): wipe the per-node icon textures and path_data so
* the render dispatcher will lazily re-resolve and re-load them for
* currently-visible entries, under its per-frame cap. Doing the
* path resolution upfront for every visible entry here would cost
* one path_is_valid syscall per entry (or up to seven per entry with
* playlist_allow_non_png enabled, due to the .png/.jpg/.jpeg/.bmp/
* .tga/.webp/.webm fallback chain in gfx_thumbnail_update_path) — enough to
* blow a frame budget on the populate frame. */
xmb_unload_icon_thumbnail_textures(xmb);
if ( xmb->is_playlist
&& !string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_IMAGES_TAB))
&& !string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MUSIC_TAB))
&& !string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_TAB)))
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
}
static void xmb_list_switch(xmb_handle_t *xmb)
{
gfx_animation_ctx_entry_t anim_entry;
settings_t *settings = config_get_ptr();
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
size_t selection = 0;
int dir = -1;
unsigned remember_selection_type = settings->uints.menu_remember_selection;
unsigned xmb_system_tab = xmb_get_system_tab(xmb, (unsigned)xmb->categories_selection_ptr);
bool xmb_main_tab_selected = false;
bool horizontal_animation = settings->bools.menu_horizontal_animation && xmb->allow_horizontal_animation;
int list_delta = 0;
unsigned animation_horizontal_highlight
= settings->uints.menu_xmb_animation_horizontal_highlight;
if (xmb->categories_selection_ptr > xmb->categories_selection_ptr_old)
dir = 1;
list_delta = (int)xmb->categories_selection_ptr
- (int)xmb->categories_selection_ptr_old;
xmb->categories_active_idx += dir;
if ( (xmb_system_tab == XMB_SYSTEM_TAB_MAIN)
|| (xmb_system_tab == XMB_SYSTEM_TAB_SETTINGS))
xmb_main_tab_selected = true;
/* Restore last selection per tab */
if ( (remember_selection_type == MENU_REMEMBER_SELECTION_ALWAYS)
|| ((remember_selection_type == MENU_REMEMBER_SELECTION_PLAYLISTS) && (xmb->is_playlist))
|| ((remember_selection_type == MENU_REMEMBER_SELECTION_MAIN) && (xmb_main_tab_selected)))
xmb_tab_set_selection(xmb);
/* Selection needs to be taken after tab restore */
selection = menu_st->selection_ptr;
xmb_list_switch_horizontal_list(xmb, horizontal_animation,
animation_horizontal_highlight);
/* Horizontal tab icon scroll */
if (horizontal_animation)
{
uintptr_t cat_tag = (uintptr_t)&xmb->categories_x_pos;
anim_entry.duration = XMB_DELAY;
anim_entry.target_value = xmb->icon_spacing_horizontal * -(float)xmb->categories_selection_ptr;
anim_entry.subject = &xmb->categories_x_pos;
anim_entry.easing_enum = XMB_EASING_XY;
anim_entry.tag = cat_tag;
anim_entry.cb = NULL;
/* Kill any in-flight tween on categories_x_pos before pushing a
* new one. gfx_animation_push appends rather than replaces, so
* rapid left/right input would otherwise stack two or more tweens
* that fight each frame over the same float (last-write-wins in
* iteration order) and the visible settled position would depend
* on which one happens to be appended last. Tagging the tween
* and killing prior ones makes back-to-back transitions atomic
* and gives a reliable "settled" predicate for callers that want
* to know when the horizontal scroll is done. */
gfx_animation_kill_by_tag(&cat_tag);
if (anim_entry.subject)
gfx_animation_push(&anim_entry);
}
else
{
/* Direct-set path: also kill any in-flight tween so it doesn't
* clobber the assignment on the next animation tick. This matters
* when menu_horizontal_animation was toggled off while a previous
* tween was still running. */
uintptr_t cat_tag = (uintptr_t)&xmb->categories_x_pos;
gfx_animation_kill_by_tag(&cat_tag);
xmb->categories_x_pos = xmb->icon_spacing_horizontal * -(float)xmb->categories_selection_ptr;
}
/* Check if we are to have horizontal animations. */
if (horizontal_animation)
xmb_list_switch_new(xmb, selection_buf, list_delta, selection);
xmb->categories_active_idx_old = (unsigned)xmb->categories_selection_ptr;
if ( gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON))
{
xmb_unload_thumbnail_textures(xmb);
if (xmb->is_playlist || xmb->is_explore_list)
{
xmb_set_thumbnail_content(xmb, NULL);
xmb_update_thumbnail_image(xmb);
}
}
}
static void xmb_list_open_horizontal_list(xmb_handle_t *xmb, bool animate)
{
unsigned j;
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL) + xmb->system_tab_end;
for (j = 0; j <= list_size; j++)
{
gfx_animation_ctx_entry_t anim_entry;
float ia = 0;
xmb_node_t *node = xmb_get_node(xmb, j);
if (!node)
continue;
if (j == xmb->categories_active_idx)
ia = xmb->categories_active_alpha;
else if (xmb->depth <= 1)
ia = xmb->categories_passive_alpha;
if (animate)
{
anim_entry.duration = XMB_DELAY;
anim_entry.target_value = ia;
anim_entry.subject = &node->alpha;
anim_entry.easing_enum = XMB_EASING_ALPHA;
/* See xmb_list_switch_horizontal_list: tag playlist-node tweens
* with the owning list so runtime frees can kill them; embedded
* system-tab nodes keep the untagged (-1) sentinel. */
anim_entry.tag = (j > xmb->system_tab_end)
? (uintptr_t)&xmb->horizontal_list : (uintptr_t)-1;
anim_entry.cb = NULL;
if (anim_entry.subject)
gfx_animation_push(&anim_entry);
}
else
node->alpha = ia;
}
}
static void xmb_context_destroy_horizontal_list(xmb_handle_t *xmb)
{
unsigned i;
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL);
for (i = 0; i < list_size; i++)
{
const char *path = NULL;
xmb_node_t *node = xmb_get_userdata_from_horizontal_list(xmb, i);
if (!node)
continue;
if (!(path = xmb->horizontal_list.list[i].path))
continue;
if (string_ends_with_size(path, ".lpl", strlen(path), STRLEN_CONST(".lpl")))
{
video_driver_texture_unload(&node->icon);
video_driver_texture_unload(&node->content_icon);
}
}
}
static void xmb_init_horizontal_list(xmb_handle_t *xmb)
{
menu_displaylist_info_t info;
settings_t *settings = config_get_ptr();
const char *dir_playlist = settings->paths.directory_playlist;
bool truncate_playlist_name = settings->bools.ozone_truncate_playlist_name;
bool sort_after_truncate = settings->bools.ozone_sort_after_truncate_playlist_name;
menu_displaylist_info_init(&info);
info.list = &xmb->horizontal_list;
info.path = strdup(dir_playlist);
info.label = strdup(MENU_ENUM_LABEL_PLAYLISTS_TAB_STR);
info.exts = strldup("lpl", sizeof("lpl"));
info.type_default = FILE_TYPE_PLAIN;
info.enum_idx = MENU_ENUM_LABEL_PLAYLISTS_TAB;
if (info.path && *info.path)
{
size_t i;
if (menu_displaylist_ctl(DISPLAYLIST_DATABASE_PLAYLISTS_HORIZONTAL, &info, settings))
{
for (i = 0; i < xmb->horizontal_list.size; i++)
xmb_node_allocate_userdata(xmb, (unsigned)i);
menu_displaylist_process(&info);
}
/* Loop through list and set console names */
for (i = 0; i < xmb->horizontal_list.size; i++)
{
char playlist_file_noext[NAME_MAX_LENGTH];
char *console_name = NULL;
const char *playlist_file = xmb->horizontal_list.list[i].path;
if (!playlist_file)
{
file_list_set_alt_at_offset(&xmb->horizontal_list, i, NULL);
continue;
}
/* Remove extension */
fill_pathname(playlist_file_noext, path_basename(playlist_file), "",
sizeof(playlist_file_noext));
console_name = playlist_file_noext;
/* Truncate playlist names, if required
* > Format: "Vendor - Console"
Remove everything before the hyphen
and the subsequent space */
if (truncate_playlist_name)
{
bool hyphen_found = false;
for (;;)
{
/* Check for "- " */
if (*console_name == '\0')
break;
else if (*console_name == '-' && *(console_name + 1) == ' ')
{
hyphen_found = true;
break;
}
console_name++;
}
if (hyphen_found)
console_name += 2;
else
console_name = playlist_file_noext;
}
/* Assign console name to list */
file_list_set_alt_at_offset(&xmb->horizontal_list, i, console_name);
}
/* If playlist names were truncated and option is
* enabled, re-sort list by console name */
if ( truncate_playlist_name
&& sort_after_truncate
&& xmb->horizontal_list.size > 0)
file_list_sort_on_alt(&xmb->horizontal_list);
}
menu_displaylist_info_free(&info);
}
static void xmb_toggle_horizontal_list(xmb_handle_t *xmb)
{
unsigned i;
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL) + xmb->system_tab_end;
for (i = 0; i <= list_size; i++)
{
xmb_node_t *node = xmb_get_node(xmb, i);
if (!node)
continue;
node->alpha = 0;
node->zoom = xmb->categories_passive_zoom;
if (i == xmb->categories_active_idx)
{
node->alpha = xmb->categories_active_alpha;
node->zoom = xmb->categories_active_zoom;
}
else if (xmb->depth <= 1)
node->alpha = xmb->categories_passive_alpha;
}
}
/* File-static generation counters for async icon loads.
* Two counters: one for horizontal list playlist icons,
* one for context_reset static textures. Separate because
* horizontal list rebuilds must not invalidate context textures. */
static uint64_t xmb_icon_load_gen = 0;
static uint64_t xmb_ctx_icon_load_gen = 0;
static void xmb_context_reset_horizontal_list(xmb_handle_t *xmb)
{
unsigned i;
char iconpath[PATH_MAX_LENGTH];
char icons_path_default[PATH_MAX_LENGTH];
bool supports_rgba = (video_driver_get_disp_flags() & VIDEO_FLAG_USE_RGBA);
int depth = 1;
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL);
uintptr_t tag = (uintptr_t)&xmb->x;
uintptr_t cat_tag = (uintptr_t)&xmb->categories_x_pos;
gfx_animation_kill_by_tag(&tag);
/* Also kill tweens on categories_x_pos: a pending tween would
* otherwise clobber the direct assignment below on the next
* animation tick. */
gfx_animation_kill_by_tag(&cat_tag);
xmb->categories_x_pos = xmb->icon_spacing_horizontal * -(float)xmb->categories_selection_ptr;
if (xmb->depth > 1)
depth++;
xmb->x = xmb->icon_size * (xmb->use_ps3_layout ? 1.1f : 0.7f) * -(depth * 2 - 2);
RHMAP_FREE(xmb->playlist_db_node_map);
/* Invalidate any in-flight async icon loads */
xmb_icon_load_gen++;
fill_pathname_application_special(iconpath, sizeof(iconpath),
APPLICATION_SPECIAL_DIRECTORY_ASSETS_XMB_ICONS);
fill_pathname_join_special(icons_path_default, iconpath,
"default", sizeof(icons_path_default));
for (i = 0; i < list_size; i++)
{
const char *path = NULL;
xmb_node_t *node = xmb_get_userdata_from_horizontal_list(xmb, i);
if (!node)
if (!(node = xmb_node_allocate_userdata(xmb, i)))
continue;
if (!(path = xmb->horizontal_list.list[i].path))
continue;
if (string_ends_with_size(path, ".lpl", strlen(path), STRLEN_CONST(".lpl")))
{
size_t __len, syslen;
char sysname[NAME_MAX_LENGTH];
char texturepath[PATH_MAX_LENGTH];
const char *console_name = NULL;
RHMAP_SET_STR(xmb->playlist_db_node_map, path, node);
syslen = fill_pathname(sysname, path_basename(path), "",
sizeof(sysname));
__len = fill_pathname_join_special(texturepath, iconpath, sysname,
sizeof(texturepath));
strlcpy_lit(texturepath + __len, ".png", sizeof(texturepath) - __len);
if (!path_is_valid(texturepath))
{
__len = fill_pathname_join_special(texturepath, iconpath, "default",
sizeof(texturepath));
strlcpy_lit(texturepath + __len, ".png", sizeof(texturepath) - __len);
}
gfx_display_load_icon(texturepath, supports_rgba,
&node->icon, xmb_icon_load_gen,
&xmb_icon_load_gen);
strlcpy_lit(sysname + syslen, "-content.png", sizeof(sysname) - syslen);
fill_pathname_join_special(texturepath, iconpath, sysname,
sizeof(texturepath));
if (!path_is_valid(texturepath))
fill_pathname_join_delim(texturepath, icons_path_default,
FILE_PATH_CONTENT_BASENAME, '-', sizeof(texturepath));
gfx_display_load_icon(texturepath, supports_rgba,
&node->content_icon, xmb_icon_load_gen,
&xmb_icon_load_gen);
console_name = xmb->horizontal_list.list[i].alt
? xmb->horizontal_list.list[i].alt
: xmb->horizontal_list.list[i].path;
if (node->console_name)
free(node->console_name);
if (console_name)
node->console_name = strdup(console_name);
else
node->console_name = strdup(path);
}
else if (string_ends_with_size(xmb->horizontal_list.list[i].label, ".lvw",
strlen(xmb->horizontal_list.list[i].label), STRLEN_CONST(".lvw")))
{
/* Free any previously-set console_name before
* overwriting; matches the .lpl branch above.
* xmb_context_reset_horizontal_list is called on every
* theme change / refresh, so without this free the
* console_name from the previous reset leaks. */
if (node->console_name)
free(node->console_name);
node->console_name = strdup(path + strlen(msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_VIEW)) + 2);
node->icon = xmb->textures.list[XMB_TEXTURE_CURSOR];
}
}
xmb_toggle_horizontal_list(xmb);
}
static void xmb_refresh_horizontal_list(xmb_handle_t *xmb)
{
struct menu_state *menu_st = menu_state_get_ptr();
xmb_context_destroy_horizontal_list(xmb);
/* Free the db_node_map BEFORE the nodes it borrows from.
* The map's values are non-owning pointers into the
* horizontal_list's userdata slots, so once those slots are
* free()d the map's stored pointers are dangling. Reverse
* the order to keep the dangling-pointer window closed. */
RHMAP_FREE(xmb->playlist_db_node_map);
xmb_free_list_nodes(&xmb->horizontal_list, false);
file_list_deinitialize(&xmb->horizontal_list);
menu_st->flags |= MENU_ST_FLAG_PREVENT_POPULATE;
xmb_init_horizontal_list(xmb);
xmb_context_reset_horizontal_list(xmb);
}
static void xmb_refresh_system_tabs_list(xmb_handle_t *xmb)
{
settings_t *settings = config_get_ptr();
xmb->system_tab_end = 0;
xmb->tabs[xmb->system_tab_end] = XMB_SYSTEM_TAB_MAIN;
if ( settings->bools.menu_content_show_settings
&& !settings->bools.kiosk_mode_enable)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_SETTINGS;
if (settings->bools.menu_content_show_favorites_first)
{
if (settings->bools.menu_content_show_favorites)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_FAVORITES;
if (settings->bools.menu_content_show_history && settings->bools.history_list_enable)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_HISTORY;
}
else
{
if (settings->bools.menu_content_show_history && settings->bools.history_list_enable)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_HISTORY;
if (settings->bools.menu_content_show_favorites)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_FAVORITES;
}
#ifdef HAVE_IMAGEVIEWER
if (settings->bools.menu_content_show_images && settings->bools.history_list_enable)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_IMAGES;
#endif
if (settings->bools.menu_content_show_music && settings->bools.history_list_enable)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_MUSIC;
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
if (settings->bools.menu_content_show_video && settings->bools.history_list_enable)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_VIDEO;
#endif
#ifdef HAVE_NETWORKING
if (settings->uints.menu_content_show_netplay == MENU_ADD_CONTENT_ENTRY_DISPLAY_PLAYLISTS_TAB)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_NETPLAY;
#endif
if ( settings->uints.menu_content_show_add_entry == MENU_ADD_CONTENT_ENTRY_DISPLAY_PLAYLISTS_TAB
&& !settings->bools.kiosk_mode_enable)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_ADD;
#if defined(HAVE_DYNAMIC)
if ( settings->uints.menu_content_show_contentless_cores
!= MENU_CONTENTLESS_CORES_DISPLAY_NONE)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_CONTENTLESS_CORES;
#endif
#if defined(HAVE_LIBRETRODB)
if (settings->bools.menu_content_show_explore)
xmb->tabs[++xmb->system_tab_end] = XMB_SYSTEM_TAB_EXPLORE;
#endif
}
static int xmb_environ(enum menu_environ_cb type, void *data, void *userdata)
{
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
if (!xmb)
return -1;
switch (type)
{
case MENU_ENVIRON_ENABLE_MOUSE_CURSOR:
xmb->show_mouse = true;
break;
case MENU_ENVIRON_DISABLE_MOUSE_CURSOR:
xmb->show_mouse = false;
break;
case MENU_ENVIRON_RESET_HORIZONTAL_LIST:
xmb_refresh_system_tabs_list(xmb);
xmb_refresh_horizontal_list(xmb);
break;
case MENU_ENVIRON_ENABLE_SCREENSAVER:
xmb->show_screensaver = true;
break;
case MENU_ENVIRON_DISABLE_SCREENSAVER:
xmb->show_screensaver = false;
break;
default:
return -1;
}
return 0;
}
static void xmb_list_open(xmb_handle_t *xmb,
unsigned animation_opening_main_menu,
bool horizontal_animation,
bool st_thumbnail_enable)
{
gfx_animation_ctx_entry_t entry;
uintptr_t tag;
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
size_t selection = menu_st->selection_ptr;
int dir = 0;
xmb->depth = (int)xmb_list_get_size(xmb, MENU_LIST_PLAIN);
if (xmb->depth > xmb->old_depth)
dir = 1;
else if (xmb->depth < xmb->old_depth)
dir = -1;
else
return; /* If menu hasn't changed, do nothing */
xmb_list_open_horizontal_list(xmb, horizontal_animation);
xmb_list_open_new(xmb, horizontal_animation, st_thumbnail_enable,
selection_buf, dir, selection);
/* Main Menu opening animation */
/* Align icon size with left thumbnail area */
entry.target_value = xmb->icon_size * (xmb->use_ps3_layout ? 1.1f : 0.7f) * -(xmb->depth * 2 - 2);
entry.subject = &xmb->x;
tag = (uintptr_t)&xmb->x;
entry.tag = tag;
entry.cb = NULL;
switch (animation_opening_main_menu)
{
case 0:
entry.easing_enum = EASING_OUT_QUAD;
entry.duration = XMB_DELAY;
break;
case 1:
entry.easing_enum = EASING_OUT_CIRC;
entry.duration = XMB_DELAY * 2;
break;
case 2:
entry.easing_enum = EASING_OUT_EXPO;
entry.duration = XMB_DELAY * 3;
break;
case 3:
entry.easing_enum = EASING_OUT_BOUNCE;
entry.duration = XMB_DELAY * 4;
break;
}
if (xmb->depth <= 2)
{
if (horizontal_animation)
gfx_animation_push(&entry);
else
xmb->x = entry.target_value;
}
xmb->old_depth = xmb->depth;
}
/* Is called whenever the list/sub-list changes */
static void xmb_populate_entries(void *data,
const char *path, const char *label, unsigned k)
{
unsigned xmb_system_tab, xmb_horizontal_type;
xmb_handle_t *xmb = (xmb_handle_t*)data;
settings_t *settings = config_get_ptr();
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
bool playlist_show_entry_idx = settings->bools.playlist_show_entry_idx;
bool was_db_manager_list = false;
int depth = (unsigned)xmb_list_get_size(xmb, MENU_LIST_PLAIN);
if (!xmb)
return;
xmb->show_playlist_tabs = settings->bools.menu_content_show_playlist_tabs;
xmb->show_horizontal_list = settings->bools.menu_xmb_show_horizontal_list;
xmb->skip_thumbnail_reset = false;
if (xmb->is_quick_menu && depth < xmb->old_depth)
xmb->skip_thumbnail_reset = true;
xmb_system_tab = xmb_get_system_tab(xmb, (unsigned)xmb->categories_selection_ptr);
xmb_horizontal_type = ((xmb_system_tab == UINT_MAX)
? xmb_get_horizontal_selection_type(xmb) : 0);
/* Determine whether this is a playlist */
xmb->is_playlist =
( depth == 1
&& ( (xmb_system_tab == XMB_SYSTEM_TAB_FAVORITES)
|| (xmb_system_tab == XMB_SYSTEM_TAB_HISTORY)
#ifdef HAVE_IMAGEVIEWER
|| (xmb_system_tab == XMB_SYSTEM_TAB_IMAGES)
#endif
|| (xmb_system_tab == XMB_SYSTEM_TAB_MUSIC)
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
|| (xmb_system_tab == XMB_SYSTEM_TAB_VIDEO)
#endif
)
)
|| string_is_equal(label, MENU_ENUM_LABEL_LOAD_CONTENT_HISTORY_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_PLAYLIST_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_FAVORITES_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_IMAGES_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_MUSIC_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_VIDEO_LIST_STR);
xmb->is_playlist |=
(xmb_horizontal_type == FILE_TYPE_PLAYLIST_COLLECTION)
&& ( (xmb_system_tab > XMB_SYSTEM_TAB_SETTINGS && depth == 1)
|| (xmb_system_tab < XMB_SYSTEM_TAB_SETTINGS && depth == 4));
xmb->is_playlist =
xmb->is_playlist
&& !string_is_equal(label, MENU_ENUM_LABEL_INFORMATION_STR)
&& !string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_RDB_ENTRY_DETAIL_STR);
xmb->is_playlist_tab = !xmb->is_playlist && string_is_equal(label,
MENU_ENUM_LABEL_PLAYLISTS_TAB_STR);
xmb->is_playlist_information =
string_is_equal(label, MENU_ENUM_LABEL_INFORMATION_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_RDB_ENTRY_DETAIL_STR);
/* Determine whether this is a database manager list */
was_db_manager_list = xmb->is_db_manager_list && depth >= 4;
xmb->is_db_manager_list = string_is_equal(label,
MENU_ENUM_LABEL_DEFERRED_DATABASE_MANAGER_LIST_STR);
if (was_db_manager_list)
{
xmb->is_db_manager_list = true;
xmb->skip_thumbnail_reset = true;
}
/* Determine whether this is the contentless cores menu */
xmb->is_contentless_cores =
string_is_equal(label,
MENU_ENUM_LABEL_CONTENTLESS_CORES_TAB_STR)
|| string_is_equal(label,
MENU_ENUM_LABEL_DEFERRED_CONTENTLESS_CORES_LIST_STR);
/* Determine whether this is a 'file list'
* (needed for handling thumbnails when viewing images
* via 'load content')
* > Note: MENU_ENUM_LABEL_FAVORITES is always set
* as the 'label' when navigating directories after
* selecting 'load content' */
xmb->is_file_list =
string_is_equal(label, MENU_ENUM_LABEL_FAVORITES_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_MENU_WALLPAPER_STR);
/* Determine whether this is the quick menu */
xmb->is_quick_menu =
string_is_equal(label,
MENU_ENUM_LABEL_DEFERRED_RPL_ENTRY_ACTIONS_STR)
|| string_is_equal(label,
MENU_ENUM_LABEL_CONTENT_SETTINGS_STR)
|| string_is_equal(label,
MENU_ENUM_LABEL_SAVESTATE_LIST_STR);
xmb->is_state_slot =
string_to_unsigned(path) == MENU_ENUM_LABEL_STATE_SLOT
|| string_is_equal(label, MENU_ENUM_LABEL_STATE_SLOT_RUN_STR);
#if defined(HAVE_LIBRETRODB)
/* Explore list */
xmb->is_explore_list =
string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_EXPLORE_LIST_STR)
|| string_is_equal(label, MENU_ENUM_LABEL_EXPLORE_TAB_STR)
|| xmb_horizontal_type == MENU_EXPLORE_TAB;
if (xmb->is_explore_list)
{
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
entry.flags |= MENU_ENTRY_FLAG_LABEL_ENABLED;
menu_entry_get(&entry, 0, 0, NULL, true);
/* Quick Menu under Explore list must also be Quick Menu */
if ( string_is_equal(entry.label, MENU_ENUM_LABEL_RUN_STR)
|| string_is_equal(entry.label, MENU_ENUM_LABEL_RESUME_CONTENT_STR)
|| string_is_equal(entry.label, MENU_ENUM_LABEL_STATE_SLOT_STR)
)
{
xmb->is_quick_menu = true;
xmb->is_explore_list = false;
}
else if (!xmb->is_quick_menu && depth < xmb->old_depth)
xmb->skip_thumbnail_reset = true;
/* 'is_playlist' must be cleared for 'xmb_set_thumbnail_content' */
if (xmb->is_explore_list)
xmb->is_playlist = false;
}
#endif
if ((menu_st->flags & MENU_ST_FLAG_PREVENT_POPULATE) > 0)
{
xmb_selection_pointer_changed(xmb, false);
menu_st->flags &= ~MENU_ST_FLAG_PREVENT_POPULATE;
/* Refresh title for search terms */
xmb_set_title(xmb);
return;
}
if (xmb->categories_selection_ptr != xmb->categories_active_idx_old)
xmb_list_switch(xmb);
else
xmb_list_open(xmb,
settings->uints.menu_xmb_animation_opening_main_menu,
settings->bools.menu_horizontal_animation && xmb->allow_horizontal_animation,
settings->bools.savestate_thumbnail_enable);
xmb_set_title(xmb);
if (xmb->is_playlist)
{
if (settings->uints.menu_icon_thumbnails)
{
/* Defer the expensive dynamic-icon unload + path_data refresh
* when the user is currently mashing or holding a navigation
* button (menu_st->scroll.acceleration > 0). Running it for
* every intermediate tab during rapid left/right traversal
* is pure waste — the work gets clobbered by the next tab's
* populate ~16 ms later before any of it becomes visible.
* xmb_render fires the deferred call on the next frame where
* acceleration has dropped to 0 (i.e. input has settled on
* whichever tab the user actually stopped on). */
if (menu_st->scroll.acceleration > 0)
xmb->pending_dynamic_icons_repopulate = true;
else
xmb_populate_dynamic_icons(xmb);
}
}
else if (xmb->thumbnails.pending_icons != XMB_PENDING_THUMBNAIL_NONE)
xmb_unload_icon_thumbnail_textures(xmb);
if (xmb->allow_dynamic_wallpaper)
xmb_update_dynamic_wallpaper(xmb, false);
/* Determine whether to show entry index */
xmb->entry_index_str[0] = '\0';
xmb->entry_idx_enabled = playlist_show_entry_idx;
if ( !xmb->is_quick_menu
&& (xmb->is_playlist || xmb->is_explore_list))
{
size_t entry_idx_selection = menu_st->selection_ptr + 1;
size_t list_size = MENU_LIST_GET_SELECTION(menu_list, 0)->size;
unsigned entry_idx_offset = 0;
playlist_show_entry_idx = (xmb->is_playlist || xmb->is_explore_list)
? playlist_show_entry_idx : false;
if (xmb->is_explore_list)
{
entry_idx_offset = 2;
if ( string_is_equal(label, MENU_ENUM_LABEL_DEFERRED_EXPLORE_LIST_STR)
|| xmb_horizontal_type == MENU_EXPLORE_TAB)
entry_idx_offset = 1;
if (entry_idx_selection > entry_idx_offset)
entry_idx_selection -= entry_idx_offset;
else
playlist_show_entry_idx = false;
if (list_size >= entry_idx_offset)
list_size -= entry_idx_offset;
}
xmb->list_size = list_size;
xmb->entry_index_offset = entry_idx_offset;
if (!playlist_show_entry_idx)
xmb->entry_index_str[0] = '\0';
else
snprintf(xmb->entry_index_str, sizeof(xmb->entry_index_str),
"%lu/%lu", (unsigned long)entry_idx_selection,
(unsigned long)list_size);
}
/* Cache previous state to prevent thumbnail flashing */
xmb->thumbnails_left_status_prev = xmb->thumbnails.left.status;
xmb->thumbnails_right_status_prev = xmb->thumbnails.right.status;
/* By default, fullscreen thumbnails are only
* enabled on playlists, database manager
* lists and file lists, in cases where ordinary
* thumbnails would normally be shown
* > This is refined on a case-by-case basis
* inside xmb_set_thumbnail_content() */
xmb->fullscreen_thumbnails_available =
(xmb->is_playlist || xmb->is_db_manager_list || xmb->is_file_list)
&& !(xmb_system_tab > XMB_SYSTEM_TAB_SETTINGS && xmb->depth > 2);
if ( (xmb->is_quick_menu || xmb->is_state_slot)
&& *xmb->savestate_thumbnail_file_path)
{
xmb->fullscreen_thumbnails_available = true;
}
else if (xmb->is_quick_menu)
xmb->fullscreen_thumbnails_available = !menu_is_running_quick_menu();
else if (xmb->is_explore_list)
{
xmb_set_thumbnail_content(xmb, "");
xmb_update_thumbnail_image(xmb);
}
/* Hack: XMB gets into complete muddle when
* performing 'complex' directory navigation
* via 'load content'. We have to work around
* this by resetting thumbnails whenever a
* file list is populated... */
else if (xmb->is_file_list)
{
xmb->fullscreen_thumbnails_available = false;
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
gfx_thumbnail_set_content(menu_st->thumbnail_path_data, NULL);
gfx_thumbnail_cancel_pending_requests();
gfx_thumbnail_reset(&xmb->thumbnails.right);
gfx_thumbnail_reset(&xmb->thumbnails.left);
gfx_thumbnail_reset(&xmb->thumbnails.icon);
xmb_set_thumbnail_content(xmb, "imageviewer");
xmb_update_thumbnail_image(xmb);
}
}
#define TEXTURE_RETROPAD(id) \
switch (id) \
{ \
case RETRO_DEVICE_ID_JOYPAD_UP: \
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_U]; \
case RETRO_DEVICE_ID_JOYPAD_DOWN: \
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_D]; \
case RETRO_DEVICE_ID_JOYPAD_LEFT: \
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_L]; \
case RETRO_DEVICE_ID_JOYPAD_RIGHT: \
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_R]; \
case RETRO_DEVICE_ID_JOYPAD_B: \
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D]; \
case RETRO_DEVICE_ID_JOYPAD_A: \
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R]; \
case RETRO_DEVICE_ID_JOYPAD_Y: \
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_L]; \
case RETRO_DEVICE_ID_JOYPAD_X: \
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_U]; \
case RETRO_DEVICE_ID_JOYPAD_SELECT: \
return xmb->textures.list[XMB_TEXTURE_INPUT_SELECT]; \
case RETRO_DEVICE_ID_JOYPAD_START: \
return xmb->textures.list[XMB_TEXTURE_INPUT_START]; \
case RETRO_DEVICE_ID_JOYPAD_L: \
return xmb->textures.list[XMB_TEXTURE_INPUT_LB]; \
case RETRO_DEVICE_ID_JOYPAD_R: \
return xmb->textures.list[XMB_TEXTURE_INPUT_RB]; \
case RETRO_DEVICE_ID_JOYPAD_L2: \
return xmb->textures.list[XMB_TEXTURE_INPUT_LT]; \
case RETRO_DEVICE_ID_JOYPAD_R2: \
return xmb->textures.list[XMB_TEXTURE_INPUT_RT]; \
case RETRO_DEVICE_ID_JOYPAD_L3: \
case RETRO_DEVICE_ID_JOYPAD_R3: \
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_P]; \
case 19: /* Left Analog Up */ \
case 23: /* Right Analog Up */ \
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_U]; \
case 18: /* Left Analog Down */ \
case 22: /* Right Analog Down */ \
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_D]; \
case 17: /* Left Analog Left */ \
case 21: /* Right Analog Left */ \
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_L]; \
case 16: /* Left Analog Right */ \
case 20: /* Right Analog Right */ \
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_R]; \
default: \
break; \
} \
static uintptr_t xmb_icon_get_id(xmb_handle_t *xmb,
xmb_node_t *core_node, xmb_node_t *node,
enum msg_hash_enums enum_idx, const char *enum_path,
const char *enum_label, unsigned type, bool active,
bool checked)
{
switch (enum_idx)
{
case MENU_ENUM_LABEL_MAIN_MENU:
return xmb->textures.list[XMB_TEXTURE_MAIN_MENU];
case MENU_ENUM_LABEL_SETTINGS_TAB:
case MENU_ENUM_LABEL_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_SETTINGS];
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_OPTIONS:
return xmb->textures.list[XMB_TEXTURE_CORE_OPTIONS];
case MENU_ENUM_LABEL_CORE_OPTION_OVERRIDE_LIST:
case MENU_ENUM_LABEL_REMAP_FILE_MANAGER_LIST:
return xmb->textures.list[XMB_TEXTURE_SETTING];
case MENU_ENUM_LABEL_ADD_TO_FAVORITES:
case MENU_ENUM_LABEL_ADD_TO_FAVORITES_PLAYLIST:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_ADD_TO_FAVORITES:
return xmb->textures.list[XMB_TEXTURE_ADD_FAVORITE];
case MENU_ENUM_LABEL_ADD_TO_PLAYLIST:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_ADD_TO_PLAYLIST:
return xmb->textures.list[XMB_TEXTURE_PLAYLIST];
case MENU_ENUM_LABEL_CREATE_NEW_PLAYLIST:
case MENU_ENUM_LABEL_SIDELOAD_CORE_LIST:
return xmb->textures.list[XMB_TEXTURE_ADD];
case MENU_ENUM_LABEL_PARENT_DIRECTORY:
case MENU_ENUM_LABEL_UNDO_LOAD_STATE:
case MENU_ENUM_LABEL_UNDO_SAVE_STATE:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_UNDO_SAVE_LOAD_STATE:
case MENU_ENUM_LABEL_RESET_CORE_ASSOCIATION:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_RESET_CORE_ASSOCIATION:
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_RESET_CORES:
return xmb->textures.list[XMB_TEXTURE_UNDO];
case MENU_ENUM_LABEL_CORE_INPUT_REMAPPING_OPTIONS:
return xmb->textures.list[XMB_TEXTURE_INPUT_REMAPPING_OPTIONS];
case MENU_ENUM_LABEL_CORE_CHEAT_OPTIONS:
return xmb->textures.list[XMB_TEXTURE_CHEAT_OPTIONS];
case MENU_ENUM_LABEL_DISK_OPTIONS:
case MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_DISK_INDEX:
case MENU_ENUM_LABEL_DISK_INDEX:
return xmb->textures.list[XMB_TEXTURE_DISK_OPTIONS];
case MENU_ENUM_LABEL_DISK_TRAY_EJECT:
case MENU_ENUM_LABEL_DISK_TRAY_INSERT:
return xmb->textures.list[XMB_TEXTURE_RELOAD];
case MENU_ENUM_LABEL_DISK_IMAGE_APPEND:
return xmb->textures.list[XMB_TEXTURE_MENU_ADD];
case MENU_ENUM_LABEL_SHADER_OPTIONS:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SHADERS:
return xmb->textures.list[XMB_TEXTURE_SHADER_OPTIONS];
case MENU_ENUM_LABEL_ACHIEVEMENT_LIST:
return xmb->textures.list[XMB_TEXTURE_ACHIEVEMENT_LIST];
case MENU_ENUM_LABEL_SAVESTATE_LIST:
case MENU_ENUM_LABEL_SAVE_STATE:
case MENU_ENUM_LABEL_CORE_CREATE_BACKUP:
case MENU_ENUM_LABEL_GAME_SPECIFIC_CORE_OPTIONS_CREATE:
case MENU_ENUM_LABEL_FOLDER_SPECIFIC_CORE_OPTIONS_CREATE:
case MENU_ENUM_LABEL_REMAP_FILE_SAVE_CORE:
case MENU_ENUM_LABEL_REMAP_FILE_SAVE_CONTENT_DIR:
case MENU_ENUM_LABEL_REMAP_FILE_SAVE_GAME:
case MENU_ENUM_LABEL_SAVE_CURRENT_CONFIG_OVERRIDE_CORE:
case MENU_ENUM_LABEL_SAVE_CURRENT_CONFIG_OVERRIDE_CONTENT_DIR:
case MENU_ENUM_LABEL_SAVE_CURRENT_CONFIG_OVERRIDE_GAME:
case MENU_ENUM_LABEL_NETWORK_ON_DEMAND_THUMBNAILS:
return xmb->textures.list[XMB_TEXTURE_SAVESTATE];
case MENU_ENUM_LABEL_LOAD_STATE:
case MENU_ENUM_LABEL_STATE_SLOT_RUN:
case MENU_ENUM_LABEL_CONFIGURATIONS:
case MENU_ENUM_LABEL_GAME_SPECIFIC_OPTIONS:
case MENU_ENUM_LABEL_REMAP_FILE_LOAD:
case MENU_ENUM_LABEL_OVERRIDE_FILE_LOAD:
case MENU_ENUM_LABEL_AUTO_OVERRIDES_ENABLE:
case MENU_ENUM_LABEL_AUTO_REMAPS_ENABLE:
case MENU_ENUM_LABEL_INITIAL_DISK_CHANGE_ENABLE:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_PREPEND:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_APPEND:
case MENU_ENUM_LABEL_CHEAT_FILE_LOAD:
case MENU_ENUM_LABEL_CHEAT_FILE_LOAD_APPEND:
case MENU_ENUM_LABEL_CORE_RESTORE_BACKUP_LIST:
return xmb->textures.list[XMB_TEXTURE_LOADSTATE];
case MENU_ENUM_LABEL_TAKE_SCREENSHOT:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_TAKE_SCREENSHOT:
return xmb->textures.list[XMB_TEXTURE_SCREENSHOT];
case MENU_ENUM_LABEL_CORE_LIST_UNLOAD:
case MENU_ENUM_LABEL_DELETE_ENTRY:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_CLOSE_CONTENT:
return xmb->textures.list[XMB_TEXTURE_CLOSE];
case MENU_ENUM_LABEL_RESTART_CONTENT:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_RESTART_CONTENT:
case MENU_ENUM_LABEL_REBOOT:
case MENU_ENUM_LABEL_CHEAT_COPY_AFTER:
case MENU_ENUM_LABEL_CHEAT_COPY_BEFORE:
case MENU_ENUM_LABEL_CHEAT_RELOAD_CHEATS:
case MENU_ENUM_LABEL_RESTART_RETROARCH:
case MENU_ENUM_LABEL_CLOUD_SYNC_SYNC_NOW:
case MENU_ENUM_LABEL_FRAME_TIME_COUNTER_SETTINGS:
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_CLEAN_PLAYLIST:
case MENU_ENUM_LABEL_PLAYLIST_MANAGER_REFRESH_PLAYLIST:
case MENU_ENUM_LABEL_CLOUD_SYNC_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_RELOAD];
case MENU_ENUM_LABEL_VRR_RUNLOOP_ENABLE:
/* Only show icon in Throttle settings */
if (xmb->depth < 3)
return xmb->textures.list[XMB_TEXTURE_RELOAD];
break;
case MENU_ENUM_LABEL_RENAME_ENTRY:
return xmb->textures.list[XMB_TEXTURE_RENAME];
case MENU_ENUM_LABEL_RESUME_CONTENT:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_RESUME_CONTENT:
return xmb->textures.list[XMB_TEXTURE_RESUME];
case MENU_ENUM_LABEL_DIRECTORY_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_DIRECTORY:
case MENU_ENUM_LABEL_SCAN_DIRECTORY:
case MENU_ENUM_LABEL_MANUAL_CONTENT_SCAN_LIST:
case MENU_ENUM_LABEL_FAVORITES: /* "Start Directory" */
case MENU_ENUM_LABEL_DOWNLOADED_FILE_DETECT_CORE_LIST:
case MENU_ENUM_LABEL_DOWNLOAD_CORE_CONTENT_DIRS:
return xmb->textures.list[XMB_TEXTURE_FOLDER];
case MENU_ENUM_LABEL_ADD_CONTENT_LIST:
if (xmb->depth > 1 || string_is_equal(enum_label, MENU_ENUM_LABEL_ADD_TAB_STR))
return xmb->textures.list[XMB_TEXTURE_ADD];
return xmb->textures.list[XMB_TEXTURE_MENU_ADD];
case MENU_ENUM_LABEL_FILE_DETECT_CORE_LIST_PUSH_DIR:
case MENU_ENUM_LABEL_VALUE_CONTENTLESS_CORES_TAB:
return xmb->textures.list[XMB_TEXTURE_RDB];
/* Menu collection submenus */
case MENU_ENUM_LABEL_PLAYLISTS_TAB:
return xmb->textures.list[XMB_TEXTURE_PLAYLIST];
case MENU_ENUM_LABEL_LOAD_CONTENT_HISTORY:
return xmb->textures.list[XMB_TEXTURE_HISTORY];
case MENU_ENUM_LABEL_GOTO_FAVORITES:
return xmb->textures.list[XMB_TEXTURE_FAVORITES];
#ifdef HAVE_IMAGEVIEWER
case MENU_ENUM_LABEL_GOTO_IMAGES:
return xmb->textures.list[XMB_TEXTURE_IMAGES];
#endif
case MENU_ENUM_LABEL_GOTO_MUSIC:
return xmb->textures.list[XMB_TEXTURE_MUSICS];
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
case MENU_ENUM_LABEL_GOTO_VIDEO:
return xmb->textures.list[XMB_TEXTURE_MOVIES];
#endif
case MENU_ENUM_LABEL_GOTO_EXPLORE:
if (xmb->depth > 1 && !string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_GOTO_EXPLORE)))
return xmb->textures.list[XMB_TEXTURE_CURSOR];
return xmb->textures.list[XMB_TEXTURE_RDB];
case MENU_ENUM_LABEL_GOTO_CONTENTLESS_CORES:
return xmb->textures.list[XMB_TEXTURE_CORE];
case MENU_ENUM_LABEL_LOAD_DISC:
case MENU_ENUM_LABEL_DUMP_DISC:
#ifdef HAVE_LAKKA
case MENU_ENUM_LABEL_EJECT_DISC:
#endif
case MENU_ENUM_LABEL_DISC_INFORMATION:
return xmb->textures.list[XMB_TEXTURE_DISC];
case MENU_ENUM_LABEL_CONTENT_SETTINGS:
case MENU_ENUM_LABEL_UPDATE_ASSETS:
return xmb->textures.list[XMB_TEXTURE_QUICKMENU];
case MENU_ENUM_LABEL_START_CORE:
case MENU_ENUM_LABEL_CHEAT_START_OR_CONT:
return xmb->textures.list[XMB_TEXTURE_RUN];
case MENU_ENUM_LABEL_CORE_LIST:
case MENU_ENUM_LABEL_CORE_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_CORE:
case MENU_ENUM_LABEL_CORE_UPDATER_LIST:
case MENU_ENUM_LABEL_CORE_MANAGER_LIST:
case MENU_ENUM_LABEL_CORE_MANAGER_ENTRY:
case MENU_ENUM_LABEL_UPDATE_INSTALLED_CORES:
case MENU_ENUM_LABEL_SWITCH_INSTALLED_CORES_PFD:
case MENU_ENUM_LABEL_SET_CORE_ASSOCIATION:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SET_CORE_ASSOCIATION:
case MENU_ENUM_LABEL_CORE_INFORMATION:
return xmb->textures.list[XMB_TEXTURE_CORE];
case MENU_ENUM_LABEL_CORE_INFO_ENTRY:
if (strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MISSING_REQUIRED)))
return xmb->textures.list[XMB_TEXTURE_CLOSE];
else if (strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MISSING_OPTIONAL)))
return xmb->textures.list[XMB_TEXTURE_INFO];
else if (strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PRESENT_REQUIRED))
|| strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PRESENT_OPTIONAL)))
return xmb->textures.list[XMB_TEXTURE_CHECKMARK];
break;
case MENU_ENUM_LABEL_LOAD_CONTENT_LIST:
case MENU_ENUM_LABEL_SUBSYSTEM_SETTINGS:
case MENU_ENUM_LABEL_SCAN_FILE:
case MENU_ENUM_LABEL_DOWNLOAD_CORE_SYSTEM_FILES:
return xmb->textures.list[XMB_TEXTURE_FILE];
case MENU_ENUM_LABEL_ONLINE_UPDATER:
case MENU_ENUM_LABEL_UPDATER_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_UPDATER];
case MENU_ENUM_LABEL_UPDATE_LAKKA:
return xmb->textures.list[XMB_TEXTURE_MAIN_MENU];
case MENU_ENUM_LABEL_UPDATE_CHEATS:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_CHEATS:
return xmb->textures.list[XMB_TEXTURE_CHEAT_OPTIONS];
case MENU_ENUM_LABEL_PL_THUMBNAILS_UPDATER_LIST:
case MENU_ENUM_LABEL_DOWNLOAD_PL_ENTRY_THUMBNAILS:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_DOWNLOAD_THUMBNAILS:
return xmb->textures.list[XMB_TEXTURE_IMAGE];
case MENU_ENUM_LABEL_UPDATE_OVERLAYS:
case MENU_ENUM_LABEL_ONSCREEN_OVERLAY_SETTINGS:
case MENU_ENUM_LABEL_CONTENT_SHOW_OVERLAYS:
return xmb->textures.list[XMB_TEXTURE_OVERLAY];
case MENU_ENUM_LABEL_OVERLAY_LIGHTGUN_SETTINGS:
case MENU_ENUM_LABEL_OVERLAY_MOUSE_SETTINGS:
case MENU_ENUM_LABEL_OSK_OVERLAY_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_SETTING];
case MENU_ENUM_LABEL_UPDATE_CG_SHADERS:
case MENU_ENUM_LABEL_UPDATE_GLSL_SHADERS:
case MENU_ENUM_LABEL_UPDATE_SLANG_SHADERS:
case MENU_ENUM_LABEL_AUTO_SHADERS_ENABLE:
case MENU_ENUM_LABEL_VIDEO_SHADER_PARAMETERS:
return xmb->textures.list[XMB_TEXTURE_SHADER_OPTIONS];
case MENU_ENUM_LABEL_INFORMATION:
case MENU_ENUM_LABEL_INFORMATION_LIST:
case MENU_ENUM_LABEL_SYSTEM_INFORMATION:
case MENU_ENUM_LABEL_UPDATE_CORE_INFO_FILES:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_INFORMATION:
return xmb->textures.list[XMB_TEXTURE_INFO];
case MENU_ENUM_LABEL_UPDATE_DATABASES:
case MENU_ENUM_LABEL_DATABASE_MANAGER:
case MENU_ENUM_LABEL_DATABASE_MANAGER_LIST:
case MENU_ENUM_LABEL_RDB_ENTRY_DETAIL:
return xmb->textures.list[XMB_TEXTURE_RDB];
case MENU_ENUM_LABEL_CURSOR_MANAGER_LIST:
return xmb->textures.list[XMB_TEXTURE_CURSOR];
case MENU_ENUM_LABEL_HELP_LIST:
return xmb->textures.list[XMB_TEXTURE_HELP];
case MENU_ENUM_LABEL_QUIT_RETROARCH:
return xmb->textures.list[XMB_TEXTURE_EXIT];
/* Settings icons */
case MENU_ENUM_LABEL_DRIVER_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_DRIVERS:
return xmb->textures.list[XMB_TEXTURE_DRIVERS];
case MENU_ENUM_LABEL_VIDEO_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_VIDEO:
return xmb->textures.list[XMB_TEXTURE_VIDEO];
case MENU_ENUM_LABEL_AUDIO_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_AUDIO:
return xmb->textures.list[XMB_TEXTURE_AUDIO];
case MENU_ENUM_LABEL_AUDIO_MIXER_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_MIXER];
case MENU_ENUM_LABEL_CONFIGURATION_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_CONFIGURATION:
case MENU_ENUM_LABEL_SETTINGS_SHOW_FILE_BROWSER:
return xmb->textures.list[XMB_TEXTURE_SETTING];
case MENU_ENUM_LABEL_INPUT_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_INPUT:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_CONTROLS:
case MENU_ENUM_LABEL_UPDATE_AUTOCONFIG_PROFILES:
case MENU_ENUM_LABEL_INPUT_LIBRETRO_DEVICE:
case MENU_ENUM_LABEL_INPUT_DEVICE_INDEX:
case MENU_ENUM_LABEL_INPUT_RETROPAD_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_1_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_2_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_3_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_4_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_5_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_6_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_7_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_8_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_9_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_10_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_11_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_12_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_13_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_14_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_15_BINDS:
case MENU_ENUM_LABEL_INPUT_USER_16_BINDS:
case MENU_ENUM_LABEL_START_NET_RETROPAD:
return xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS];
case MENU_ENUM_LABEL_INPUT_MOUSE_INDEX:
return xmb->textures.list[XMB_TEXTURE_INPUT_MOUSE];
case MENU_ENUM_LABEL_INPUT_PLAYER_ANALOG_DPAD_MODE:
return xmb->textures.list[XMB_TEXTURE_INPUT_ADC];
case MENU_ENUM_LABEL_INPUT_TURBO_FIRE_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_INPUT_TURBO];
case MENU_ENUM_LABEL_INPUT_TURBO_BIND:
case MENU_ENUM_LABEL_INPUT_TURBO_BUTTON:
{
settings_t *settings = config_get_ptr();
int turbo_bind = settings->ints.input_turbo_bind;
if (enum_idx == MENU_ENUM_LABEL_INPUT_TURBO_BUTTON)
turbo_bind = settings->uints.input_turbo_button;
TEXTURE_RETROPAD(turbo_bind)
break;
}
case MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_DESCRIPTION:
case MENU_ENUM_LABEL_DEFERRED_DROPDOWN_BOX_LIST_INPUT_RETROPAD_BIND:
TEXTURE_RETROPAD(type)
break;
case MENU_ENUM_LABEL_LATENCY_SETTINGS:
case MENU_ENUM_LABEL_CONTENT_SHOW_LATENCY:
case MENU_ENUM_LABEL_SETTINGS_SHOW_LATENCY:
case MENU_ENUM_LABEL_MENU_THROTTLE_FRAMERATE:
return xmb->textures.list[XMB_TEXTURE_LATENCY];
case MENU_ENUM_LABEL_SAVING_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_SAVING:
case MENU_ENUM_LABEL_SAVE_CURRENT_CONFIG:
case MENU_ENUM_LABEL_SAVE_NEW_CONFIG:
case MENU_ENUM_LABEL_SAVE_AS_CONFIG:
case MENU_ENUM_LABEL_SAVE_MAIN_CONFIG:
case MENU_ENUM_LABEL_CONFIG_SAVE_ON_EXIT:
case MENU_ENUM_LABEL_REMAP_SAVE_ON_EXIT:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_CURRENT:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_AS:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_GLOBAL:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_CORE:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_PARENT:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_SAVE_GAME:
case MENU_ENUM_LABEL_CHEAT_FILE_SAVE_AS:
case MENU_ENUM_LABEL_OVERRIDE_FILE_SAVE_AS:
case MENU_ENUM_LABEL_REMAP_FILE_SAVE_AS:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SAVESTATE_SUBMENU:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SAVE_LOAD_STATE:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_REPLAY:
return xmb->textures.list[XMB_TEXTURE_SAVING];
case MENU_ENUM_LABEL_LOGGING_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_LOGGING:
return xmb->textures.list[XMB_TEXTURE_LOG];
case MENU_ENUM_LABEL_FASTFORWARD_RATIO:
case MENU_ENUM_LABEL_FRAME_THROTTLE_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_FRAME_THROTTLE:
case MENU_ENUM_LABEL_FASTFORWARD_FRAMESKIP:
return xmb->textures.list[XMB_TEXTURE_FRAMESKIP];
case MENU_ENUM_LABEL_QUICK_MENU_START_RECORDING:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_START_RECORDING:
case MENU_ENUM_LABEL_RECORDING_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_RECORDING:
return xmb->textures.list[XMB_TEXTURE_RECORD];
case MENU_ENUM_LABEL_QUICK_MENU_START_STREAMING:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_START_STREAMING:
return xmb->textures.list[XMB_TEXTURE_STREAM];
case MENU_ENUM_LABEL_QUICK_MENU_STOP_STREAMING:
case MENU_ENUM_LABEL_QUICK_MENU_STOP_RECORDING:
case MENU_ENUM_LABEL_CHEAT_DELETE:
case MENU_ENUM_LABEL_CHEAT_DELETE_ALL:
case MENU_ENUM_LABEL_CHEAT_DELETE_MATCH:
case MENU_ENUM_LABEL_CORE_DELETE:
case MENU_ENUM_LABEL_DELETE_PLAYLIST:
case MENU_ENUM_LABEL_CORE_DELETE_BACKUP_LIST:
case MENU_ENUM_LABEL_VIDEO_FILTER_REMOVE:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_REMOVE_GLOBAL:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_REMOVE_CORE:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_REMOVE_PARENT:
case MENU_ENUM_LABEL_VIDEO_SHADER_PRESET_REMOVE_GAME:
case MENU_ENUM_LABEL_AUDIO_DSP_PLUGIN_REMOVE:
case MENU_ENUM_LABEL_GAME_SPECIFIC_CORE_OPTIONS_REMOVE:
case MENU_ENUM_LABEL_FOLDER_SPECIFIC_CORE_OPTIONS_REMOVE:
case MENU_ENUM_LABEL_REMAP_FILE_REMOVE_CORE:
case MENU_ENUM_LABEL_REMAP_FILE_REMOVE_CONTENT_DIR:
case MENU_ENUM_LABEL_REMAP_FILE_REMOVE_GAME:
case MENU_ENUM_LABEL_REMOVE_CURRENT_CONFIG_OVERRIDE_CORE:
case MENU_ENUM_LABEL_REMOVE_CURRENT_CONFIG_OVERRIDE_CONTENT_DIR:
case MENU_ENUM_LABEL_REMOVE_CURRENT_CONFIG_OVERRIDE_GAME:
return xmb->textures.list[XMB_TEXTURE_CLOSE];
case MENU_ENUM_LABEL_RESET_TO_DEFAULT_CONFIG:
case MENU_ENUM_LABEL_CORE_OPTIONS_RESET:
case MENU_ENUM_LABEL_REMAP_FILE_RESET:
case MENU_ENUM_LABEL_OVERRIDE_UNLOAD:
case MENU_ENUM_LABEL_MENU_DISABLE_KIOSK_MODE:
case MENU_ENUM_LABEL_XMB_MAIN_MENU_ENABLE_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_UNDO];
case MENU_ENUM_LABEL_CORE_OPTIONS_FLUSH:
case MENU_ENUM_LABEL_REMAP_FILE_FLUSH:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_CORE_OPTIONS_FLUSH:
return xmb->textures.list[XMB_TEXTURE_FILE];
case MENU_ENUM_LABEL_CORE_LOCK:
case MENU_ENUM_LABEL_CORE_SET_STANDALONE_EXEMPT:
return xmb->textures.list[XMB_TEXTURE_CORE];
case MENU_ENUM_LABEL_ONSCREEN_DISPLAY_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_ONSCREEN_DISPLAY:
return xmb->textures.list[XMB_TEXTURE_OSD];
case MENU_ENUM_LABEL_SHOW_WIMP:
case MENU_ENUM_LABEL_USER_INTERFACE_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_USER_INTERFACE:
return xmb->textures.list[XMB_TEXTURE_UI];
#if defined(HAVE_LIBNX)
case MENU_ENUM_LABEL_SWITCH_CPU_PROFILE:
return xmb->textures.list[XMB_TEXTURE_POWER];
#endif
case MENU_ENUM_LABEL_POWER_MANAGEMENT_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_POWER_MANAGEMENT:
return xmb->textures.list[XMB_TEXTURE_POWER];
case MENU_ENUM_LABEL_RETRO_ACHIEVEMENTS_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_ACHIEVEMENTS:
return xmb->textures.list[XMB_TEXTURE_ACHIEVEMENTS];
case MENU_ENUM_LABEL_PLAYLIST_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_PLAYLISTS:
return xmb->textures.list[XMB_TEXTURE_PLAYLIST];
case MENU_ENUM_LABEL_USER_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_USER:
return xmb->textures.list[XMB_TEXTURE_USER];
case MENU_ENUM_LABEL_PRIVACY_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_PRIVACY];
case MENU_ENUM_LABEL_REWIND_SETTINGS:
case MENU_ENUM_LABEL_CONTENT_SHOW_REWIND:
return xmb->textures.list[XMB_TEXTURE_REWIND];
case MENU_ENUM_LABEL_INPUT_REMAP_PORT:
case MENU_ENUM_LABEL_INPUT_DEVICE_RESERVATION_TYPE:
case MENU_ENUM_LABEL_INPUT_DEVICE_RESERVED_DEVICE_NAME:
case MENU_ENUM_LABEL_QUICK_MENU_OVERRIDE_OPTIONS:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SAVE_CORE_OVERRIDES:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SAVE_CONTENT_DIR_OVERRIDES:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SAVE_GAME_OVERRIDES:
return xmb->textures.list[XMB_TEXTURE_OVERRIDE];
case MENU_ENUM_LABEL_ONSCREEN_NOTIFICATIONS_SETTINGS:
case MENU_ENUM_LABEL_CHEEVOS_APPEARANCE_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_NOTIFICATIONS];
#ifdef HAVE_NETWORKING
case MENU_ENUM_LABEL_NETPLAY:
return xmb->textures.list[XMB_TEXTURE_NETPLAY];
case MENU_ENUM_LABEL_NETPLAY_ENABLE_HOST:
return xmb->textures.list[XMB_TEXTURE_RUN];
case MENU_ENUM_LABEL_NETPLAY_DISCONNECT:
return xmb->textures.list[XMB_TEXTURE_CLOSE];
case MENU_ENUM_LABEL_NETPLAY_ENABLE_CLIENT:
return xmb->textures.list[XMB_TEXTURE_ROOM];
case MENU_ENUM_LABEL_NETPLAY_REFRESH_ROOMS:
return xmb->textures.list[XMB_TEXTURE_RELOAD];
#ifdef HAVE_NETPLAYDISCOVERY
case MENU_ENUM_LABEL_NETPLAY_REFRESH_LAN:
return xmb->textures.list[XMB_TEXTURE_RELOAD];
#endif
case MENU_ENUM_LABEL_NETWORK_INFORMATION:
case MENU_ENUM_LABEL_NETWORK_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_NETWORK:
case MENU_ENUM_LABEL_WIFI_SETTINGS:
case MENU_ENUM_LABEL_NETWORK_INFO_ENTRY:
case MENU_ENUM_LABEL_NETWORK_HOSTING_SETTINGS:
case MENU_ENUM_LABEL_NETPLAY_LOBBY_FILTERS:
case MENU_ENUM_LABEL_NETPLAY_KICK:
case MENU_ENUM_LABEL_NETPLAY_BAN:
return xmb->textures.list[XMB_TEXTURE_NETWORK];
#endif
case MENU_ENUM_LABEL_BLUETOOTH_SETTINGS:
return xmb->textures.list[XMB_TEXTURE_BLUETOOTH];
case MENU_ENUM_LABEL_SHUTDOWN:
return xmb->textures.list[XMB_TEXTURE_SHUTDOWN];
case MENU_ENUM_LABEL_CHEAT_APPLY_CHANGES:
case MENU_ENUM_LABEL_SHADER_APPLY_CHANGES:
return xmb->textures.list[XMB_TEXTURE_CHECKMARK];
case MENU_ENUM_LABEL_CHEAT_COPY_MATCH:
case MENU_ENUM_LABEL_CHEAT_ADD_MATCHES:
case MENU_ENUM_LABEL_CHEAT_ADD_NEW_AFTER:
case MENU_ENUM_LABEL_CHEAT_ADD_NEW_BEFORE:
case MENU_ENUM_LABEL_CHEAT_ADD_NEW_TOP:
case MENU_ENUM_LABEL_CHEAT_ADD_NEW_BOTTOM:
return xmb->textures.list[XMB_TEXTURE_MENU_ADD];
case MENU_ENUM_LABEL_CHEAT_APPLY_AFTER_TOGGLE:
return xmb->textures.list[XMB_TEXTURE_MENU_APPLY_TOGGLE];
case MENU_ENUM_LABEL_CHEAT_APPLY_AFTER_LOAD:
return xmb->textures.list[XMB_TEXTURE_MENU_APPLY_COG];
case MENU_ENUM_LABEL_SLOWMOTION_RATIO:
return xmb->textures.list[XMB_TEXTURE_RESUME];
case MENU_ENUM_LABEL_START_VIDEO_PROCESSOR:
return xmb->textures.list[XMB_TEXTURE_MOVIE];
#ifdef HAVE_LIBRETRODB
case MENU_ENUM_LABEL_EXPLORE_ITEM:
{
uintptr_t icon = menu_explore_get_entry_icon(type);
if (icon)
return icon;
else if (string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_SAVE_VIEW)))
return xmb->textures.list[XMB_TEXTURE_SAVING];
else if (string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_DELETE_VIEW)))
return xmb->textures.list[XMB_TEXTURE_CLOSE];
else if (type != FILE_TYPE_RDB)
return xmb->textures.list[XMB_TEXTURE_CURSOR];
break;
}
#endif
case MENU_ENUM_LABEL_CONTENTLESS_CORE:
{
uintptr_t icon = menu_contentless_cores_get_entry_icon(enum_label);
if (icon)
return icon;
break;
}
default:
break;
}
switch (type)
{
case FILE_TYPE_DIRECTORY:
return xmb->textures.list[XMB_TEXTURE_FOLDER];
case FILE_TYPE_PLAIN:
case FILE_TYPE_IN_CARCHIVE:
return xmb->textures.list[XMB_TEXTURE_FILE];
case FILE_TYPE_RPL_ENTRY:
case FILE_TYPE_PLAYLIST_COLLECTION:
if (core_node)
return core_node->content_icon;
switch (xmb_get_system_tab(xmb, (unsigned)xmb->categories_selection_ptr))
{
case XMB_SYSTEM_TAB_MAIN:
{
const struct playlist_entry *pl_entry = NULL;
xmb_node_t *db_node = NULL;
playlist_get_index(playlist_get_cached(), 0, &pl_entry);
if ( pl_entry
&& (pl_entry->db_name && *pl_entry->db_name)
&& (db_node = RHMAP_GET_STR(xmb->playlist_db_node_map, pl_entry->db_name)))
{
switch (type)
{
case FILE_TYPE_RPL_ENTRY:
return db_node->content_icon;
case FILE_TYPE_PLAYLIST_COLLECTION:
return db_node->icon;
default:
break;
}
}
else
{
if (string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_IMAGES_TAB)))
return xmb->textures.list[XMB_TEXTURE_IMAGE];
else if (string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MUSIC_TAB)))
return xmb->textures.list[XMB_TEXTURE_MUSIC];
else if (string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_TAB)))
return xmb->textures.list[XMB_TEXTURE_MOVIE];
}
}
break;
#ifdef HAVE_IMAGEVIEWER
case XMB_SYSTEM_TAB_IMAGES:
return xmb->textures.list[XMB_TEXTURE_IMAGE];
#endif
case XMB_SYSTEM_TAB_MUSIC:
return xmb->textures.list[XMB_TEXTURE_MUSIC];
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
case XMB_SYSTEM_TAB_VIDEO:
return xmb->textures.list[XMB_TEXTURE_MOVIE];
#endif
default:
break;
}
return xmb->textures.list[XMB_TEXTURE_FILE];
case MENU_SET_CDROM_INFO:
case MENU_SET_CDROM_LIST:
case MENU_SET_LOAD_CDROM_LIST:
return xmb->textures.list[XMB_TEXTURE_DISC];
case FILE_TYPE_SHADER:
case FILE_TYPE_SHADER_PRESET:
return xmb->textures.list[XMB_TEXTURE_SHADER_OPTIONS];
case FILE_TYPE_USE_DIRECTORY:
return xmb->textures.list[XMB_TEXTURE_CHECKMARK];
case FILE_TYPE_CARCHIVE:
return xmb->textures.list[XMB_TEXTURE_ZIP];
case FILE_TYPE_IMAGE:
case FILE_TYPE_IMAGEVIEWER:
return xmb->textures.list[XMB_TEXTURE_IMAGE];
case FILE_TYPE_MUSIC:
return xmb->textures.list[XMB_TEXTURE_MUSIC];
case FILE_TYPE_MOVIE:
return xmb->textures.list[XMB_TEXTURE_MOVIE];
case FILE_TYPE_CORE:
case FILE_TYPE_DOWNLOAD_CORE:
case FILE_TYPE_DIRECT_LOAD:
return xmb->textures.list[XMB_TEXTURE_CORE];
case FILE_TYPE_RDB:
return xmb->textures.list[XMB_TEXTURE_RDB];
case FILE_TYPE_RDB_ENTRY:
return xmb->textures.list[XMB_TEXTURE_FILE];
case FILE_TYPE_PLAYLIST_ENTRY:
case MENU_SETTING_ACTION_RUN:
return xmb->textures.list[XMB_TEXTURE_RUN];
case MENU_SETTING_ACTION_RESUME_ACHIEVEMENTS:
return xmb->textures.list[XMB_TEXTURE_RESUME];
case MENU_SETTING_ACTION_CLOSE:
case MENU_SETTING_ACTION_CLOSE_HORIZONTAL:
case MENU_SETTING_ACTION_DELETE_ENTRY:
return xmb->textures.list[XMB_TEXTURE_CLOSE];
case MENU_SETTING_ACTION_SAVESTATE:
return xmb->textures.list[XMB_TEXTURE_SAVESTATE];
case MENU_SETTING_ACTION_LOADSTATE:
return xmb->textures.list[XMB_TEXTURE_LOADSTATE];
case MENU_SETTING_ACTION_PLAYREPLAY:
return xmb->textures.list[XMB_TEXTURE_PLAYREPLAY];
case MENU_SETTING_ACTION_RECORDREPLAY:
return xmb->textures.list[XMB_TEXTURE_RECORDREPLAY];
case MENU_SETTING_ACTION_HALTREPLAY:
return xmb->textures.list[XMB_TEXTURE_HALTREPLAY];
case MENU_SETTING_ACTION_CORE_OPTIONS:
/* Exact matches first: an entry whose label *is* one of these
* must not be captured by a shorter label that happens to be
* its prefix (hu: VIDEO_SETTINGS is "Kép", and the OSD label
* "Képernyőn megjelenő elemek (OSD)" starts with it). */
if (string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ONSCREEN_DISPLAY_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_OSD];
else if (string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_LATENCY_SETTINGS))
|| string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_TIMING_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_LATENCY];
else if (string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MEDIA_SETTINGS))
|| string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_STORAGE_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_RDB];
else if (string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SYSTEM_SETTINGS))
|| string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SPECS_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_DRIVERS];
/* then prefix matches, in their original relative order */
else if (string_starts_with(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_VIDEO];
else if (string_starts_with(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_AUDIO_SETTINGS))
|| string_starts_with(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_SOUND_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_AUDIO];
else if (string_starts_with(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_INPUT_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS];
else if (string_starts_with(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_PERFORMANCE_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_FRAMESKIP];
/* then substring matches, last as before */
else if (strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_HACKS_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_POWER];
else if (strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MAPPING_SETTINGS)))
return xmb->textures.list[XMB_TEXTURE_OVERRIDE];
return xmb->textures.list[XMB_TEXTURE_CORE_OPTIONS];
case MENU_SETTING_ACTION_CORE_INPUT_REMAPPING_OPTIONS:
return xmb->textures.list[XMB_TEXTURE_INPUT_REMAPPING_OPTIONS];
case MENU_SETTING_ACTION_CORE_CHEAT_OPTIONS:
return xmb->textures.list[XMB_TEXTURE_CHEAT_OPTIONS];
case MENU_SETTING_ACTION_CORE_DISK_OPTIONS:
return xmb->textures.list[XMB_TEXTURE_DISK_OPTIONS];
case MENU_SETTING_ACTION_CORE_SHADER_OPTIONS:
return xmb->textures.list[XMB_TEXTURE_SHADER_OPTIONS];
case MENU_SETTING_ACTION_SCREENSHOT:
return xmb->textures.list[XMB_TEXTURE_SCREENSHOT];
case MENU_SETTING_ACTION_RESET:
return xmb->textures.list[XMB_TEXTURE_RELOAD];
case MENU_SETTING_ACTION_PAUSE_ACHIEVEMENTS:
return xmb->textures.list[XMB_TEXTURE_PAUSE];
case MENU_ENUM_LABEL_ACHIEVEMENT_SERVER_UNREACHABLE:
return xmb->textures.list[XMB_TEXTURE_NETWORK];
case MENU_SET_SCREEN_BRIGHTNESS:
return xmb->textures.list[XMB_TEXTURE_BRIGHTNESS];
case MENU_SETTING_GROUP:
return xmb->textures.list[XMB_TEXTURE_SETTING];
case MENU_INFO_MESSAGE:
return xmb->textures.list[XMB_TEXTURE_CORE_INFO];
case MENU_BLUETOOTH:
return xmb->textures.list[XMB_TEXTURE_BLUETOOTH];
case MENU_WIFI:
return xmb->textures.list[XMB_TEXTURE_WIFI];
#ifdef HAVE_NETWORKING
case MENU_ROOM:
return xmb->textures.list[XMB_TEXTURE_ROOM];
case MENU_ROOM_LAN:
return xmb->textures.list[XMB_TEXTURE_ROOM_LAN];
case MENU_ROOM_RELAY:
return xmb->textures.list[XMB_TEXTURE_ROOM_RELAY];
#endif
case MENU_SETTINGS_INPUT_LIBRETRO_DEVICE:
return xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS];
case MENU_SETTINGS_INPUT_ANALOG_DPAD_MODE:
return xmb->textures.list[XMB_TEXTURE_INPUT_ADC];
case MENU_SETTINGS_INPUT_INPUT_REMAP_PORT:
return xmb->textures.list[XMB_TEXTURE_OVERRIDE];
}
#ifdef HAVE_CHEEVOS
if ( type >= MENU_SETTINGS_CHEEVOS_START
&& type < MENU_SETTINGS_NETPLAY_ROOMS_START)
{
char buffer[64];
int index = type - MENU_SETTINGS_CHEEVOS_START;
uintptr_t badge_texture = rcheevos_menu_get_badge_texture(index);
if (badge_texture)
return badge_texture;
/* No state means it's a header, show info icon */
if (!rcheevos_menu_get_state(index, buffer, sizeof(buffer)))
return xmb->textures.list[XMB_TEXTURE_INFO];
/* Placeholder badge image was not found, show generic menu icon */
return xmb->textures.list[XMB_TEXTURE_ACHIEVEMENTS];
}
#endif
if ( type >= MENU_SETTINGS_INPUT_BEGIN
&& type <= MENU_SETTINGS_INPUT_DESC_KBD_END)
{
size_t enum_label_len = strlen(enum_label);
/* This part is only utilized by Input User # Binds */
if (type < MENU_SETTINGS_INPUT_DESC_BEGIN)
{
if ( string_ends_with_size(enum_label, "_joypad_index", enum_label_len, STRLEN_CONST("_joypad_index")))
return xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS];
else if (string_ends_with_size(enum_label, "_mouse_index", enum_label_len, STRLEN_CONST("_mouse_index")))
return xmb->textures.list[XMB_TEXTURE_INPUT_MOUSE];
else if (string_ends_with_size(enum_label, "_analog_dpad_mode", enum_label_len, STRLEN_CONST("_analog_dpad_mode")))
return xmb->textures.list[XMB_TEXTURE_INPUT_ADC];
else if (string_ends_with_size(enum_label, "_bind_all", enum_label_len, STRLEN_CONST("_bind_all")))
return xmb->textures.list[XMB_TEXTURE_INPUT_BIND_ALL];
else if (string_ends_with_size(enum_label, "_bind_defaults", enum_label_len, STRLEN_CONST("_bind_defaults")))
return xmb->textures.list[XMB_TEXTURE_RELOAD];
else if (string_ends_with_size(enum_label, "_save_autoconfig", enum_label_len, STRLEN_CONST("_save_autoconfig")))
return xmb->textures.list[XMB_TEXTURE_SAVING];
else if (string_ends_with_size(enum_label, "_turbo", enum_label_len, STRLEN_CONST("_turbo"))
|| string_ends_with_size(enum_label, "_hold", enum_label_len, STRLEN_CONST("_hold")))
return xmb->textures.list[XMB_TEXTURE_INPUT_TURBO];
else if (strstr(enum_label, "_gun_"))
return xmb->textures.list[XMB_TEXTURE_INPUT_LGUN];
else if (string_starts_with_size(enum_label, "input_device_reserv", STRLEN_CONST("input_device_reserv")))
return xmb->textures.list[XMB_TEXTURE_OVERRIDE];
}
/* This is used for both Input Port Binds and Quickmenu controls */
if ( string_ends_with_size(enum_label, "_up", enum_label_len, STRLEN_CONST("_up")))
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_U];
else if (string_ends_with_size(enum_label, "_down", enum_label_len, STRLEN_CONST("_down")))
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_D];
else if (string_ends_with_size(enum_label, "_left", enum_label_len, STRLEN_CONST("_left")))
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_L];
else if (string_ends_with_size(enum_label, "_right", enum_label_len, STRLEN_CONST("_right")))
return xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_R];
else if (string_ends_with_size(enum_label, "_b", enum_label_len, STRLEN_CONST("_b")))
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D];
else if (string_ends_with_size(enum_label, "_a", enum_label_len, STRLEN_CONST("_a")))
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R];
else if (string_ends_with_size(enum_label, "_y", enum_label_len, STRLEN_CONST("_y")))
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_L];
else if (string_ends_with_size(enum_label, "_x", enum_label_len, STRLEN_CONST("_x")))
return xmb->textures.list[XMB_TEXTURE_INPUT_BTN_U];
else if (string_ends_with_size(enum_label, "_select", enum_label_len, STRLEN_CONST("_select")))
return xmb->textures.list[XMB_TEXTURE_INPUT_SELECT];
else if (string_ends_with_size(enum_label, "_start", enum_label_len, STRLEN_CONST("_start")))
return xmb->textures.list[XMB_TEXTURE_INPUT_START];
else if (string_ends_with_size(enum_label, "_l", enum_label_len, STRLEN_CONST("_l")))
return xmb->textures.list[XMB_TEXTURE_INPUT_LB];
else if (string_ends_with_size(enum_label, "_r", enum_label_len, STRLEN_CONST("_r")))
return xmb->textures.list[XMB_TEXTURE_INPUT_RB];
else if (string_ends_with_size(enum_label, "_l2", enum_label_len, STRLEN_CONST("_l2")))
return xmb->textures.list[XMB_TEXTURE_INPUT_LT];
else if (string_ends_with_size(enum_label, "_r2", enum_label_len, STRLEN_CONST("_r2")))
return xmb->textures.list[XMB_TEXTURE_INPUT_RT];
else if (string_ends_with_size(enum_label, "_l3", enum_label_len, STRLEN_CONST("_l3")))
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_P];
else if (string_ends_with_size(enum_label, "_r3", enum_label_len, STRLEN_CONST("_r3")))
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_P];
else if (string_ends_with_size(enum_label, "_y_minus", enum_label_len, STRLEN_CONST("_y_minus")))
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_U];
else if (string_ends_with_size(enum_label, "_y_plus", enum_label_len, STRLEN_CONST("_y_plus")))
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_D];
else if (string_ends_with_size(enum_label, "_x_minus", enum_label_len, STRLEN_CONST("_x_minus")))
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_L];
else if (string_ends_with_size(enum_label, "_x_plus", enum_label_len, STRLEN_CONST("_x_plus")))
return xmb->textures.list[XMB_TEXTURE_INPUT_STCK_R];
}
if ( type >= MENU_SETTINGS_REMAPPING_PORT_BEGIN
&& type <= MENU_SETTINGS_REMAPPING_PORT_END)
return xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS];
if (checked)
return xmb->textures.list[XMB_TEXTURE_CHECKMARK];
if (type == MENU_SETTING_ACTION)
return xmb->textures.list[XMB_TEXTURE_SETTING];
return xmb->textures.list[XMB_TEXTURE_SUBSETTING];
}
static size_t xmb_animation_line_ticker_loop(uint64_t idx,
size_t line_len, size_t num_lines)
{
size_t line_ticks = TICKER_LINE_DISPLAY_TICKS(line_len);
size_t ticker_period = num_lines + 1;
size_t phase = (idx / line_ticks) % ticker_period;
/* In this case, line_offset is simply equal to the phase */
return phase;
}
static size_t xmb_animation_line_ticker_generic(uint64_t idx,
size_t line_len, size_t max_lines, size_t num_lines)
{
size_t line_ticks = TICKER_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
* or last line is reached (this is mostly required for the
* case where num_lines == (max_lines + 1), since otherwise
* the text flicks rapidly up and down in disconcerting
* fashion...) */
size_t ticker_period = (excess_lines * 2) + 2;
size_t phase = (idx / line_ticks) % ticker_period;
/* Pause on first line */
if (phase > 0)
phase--;
/* Pause on last line */
if (phase > excess_lines)
phase--;
/* Lines scrolling upwards */
if (phase <= excess_lines)
return phase;
/* Lines scrolling downwards */
return (excess_lines * 2) - phase;
}
XMB_NOINLINE static bool xmb_animation_line_ticker(xmb_handle_t *xmb,
gfx_animation_t *p_anim, gfx_animation_ctx_line_ticker_t *line_ticker)
{
char *wrapped_str = NULL;
size_t wrapped_str_len = 0;
size_t line_ticker_str_len = 0;
size_t line_offset = 0;
size_t total_lines = 0;
bool ret = false;
bool is_active = false;
/* Sanity check */
if (!line_ticker)
return false;
if ( (!line_ticker->str || !*line_ticker->str)
|| (line_ticker->line_len < 1)
|| (line_ticker->max_lines < 1)
|| (line_ticker->len < 1))
goto end;
/* Line wrap input string */
line_ticker_str_len = strlen(line_ticker->str);
wrapped_str_len = line_ticker_str_len + 1 + 10; /* 10 bytes use for inserting '\n' */
if (wrapped_str_len > xmb->wrap_scratch_cap)
{
char *grown = (char*)realloc(xmb->wrap_scratch, wrapped_str_len);
if (!grown)
goto end;
xmb->wrap_scratch = grown;
xmb->wrap_scratch_cap = wrapped_str_len;
}
wrapped_str = xmb->wrap_scratch;
wrapped_str[0] = '\0';
word_wrap(
wrapped_str,
wrapped_str_len,
line_ticker->str,
line_ticker_str_len,
(int)line_ticker->line_len,
100, 0);
if (!wrapped_str || !*wrapped_str)
goto end;
/* Count total number of lines */
{
size_t slen = strlen(wrapped_str);
const char *p = wrapped_str;
size_t remaining = slen;
total_lines = 1;
while (remaining > 0)
{
const char *nl = (const char *)memchr(p, '\n', remaining);
if (!nl)
break;
total_lines++;
nl++;
remaining -= (size_t)(nl - p);
p = nl;
}
}
/* Check whether total number of lines fits within
* the set limit */
if (total_lines <= line_ticker->max_lines)
{
strlcpy(line_ticker->s, wrapped_str, line_ticker->len);
ret = true;
goto end;
}
/* Determine offset of first line in wrapped string */
switch (line_ticker->type_enum)
{
case TICKER_TYPE_LOOP:
line_offset = xmb_animation_line_ticker_loop(
line_ticker->idx,
line_ticker->line_len,
total_lines);
break;
case TICKER_TYPE_BOUNCE:
default:
line_offset = xmb_animation_line_ticker_generic(
line_ticker->idx,
line_ticker->line_len,
line_ticker->max_lines,
total_lines);
break;
}
/* Build output string from required lines */
{
const char *p = wrapped_str;
size_t remaining = strlen(wrapped_str);
size_t cur_line = 0;
/* Skip to the starting line */
while (cur_line < line_offset && remaining > 0)
{
const char *nl = (const char *)memchr(p, '\n', remaining);
if (!nl)
break;
nl++;
remaining -= (size_t)(nl - p);
p = nl;
cur_line++;
}
/* Find the end of the visible window */
{
const char *end_p = p;
size_t end_remaining = remaining;
size_t lines_found = 0;
while (lines_found < line_ticker->max_lines && end_remaining > 0)
{
const char *nl = (const char *)memchr(end_p, '\n', end_remaining);
if (!nl)
{
end_p = end_p + end_remaining;
break;
}
lines_found++;
if (lines_found >= line_ticker->max_lines)
{
end_p = nl;
break;
}
nl++;
end_remaining -= (size_t)(nl - end_p);
end_p = nl;
}
/* Single memcpy for the whole block */
{
size_t copy_len = (size_t)(end_p - p);
size_t max_copy = line_ticker->len - 1;
if (copy_len > max_copy)
copy_len = max_copy;
memcpy(line_ticker->s, p, copy_len);
line_ticker->s[copy_len] = '\0';
}
}
}
ret = true;
is_active = true;
p_anim->flags |= GFX_ANIM_FLAG_TICKER_IS_ACTIVE;
end:
if (!ret)
if (line_ticker->len > 0)
line_ticker->s[0] = '\0';
return is_active;
}
static void xmb_animation_set_line_smooth_fade_parameters(
bool scroll_up,
size_t scroll_ticks, size_t line_phase, size_t line_height,
size_t num_lines, size_t num_display_lines, size_t line_offset,
float y_offset,
size_t *top_fade_line_offset,
float *top_fade_y_offset, float *top_fade_alpha,
size_t *bottom_fade_line_offset,
float *bottom_fade_y_offset, float *bottom_fade_alpha)
{
/* When a line fades out, alpha transitions from
* 1 to 0 over the course of one half of the
* scrolling line height. When a line fades in,
* it's the other way around */
float fade_out_alpha = ((float)scroll_ticks - ((float)line_phase * 2.0f)) / (float)scroll_ticks;
float fade_in_alpha = -1.0f * fade_out_alpha;
if (fade_out_alpha < 0.0f)
fade_out_alpha = 0.0f;
if (fade_in_alpha < 0.0f)
fade_in_alpha = 0.0f;
*top_fade_line_offset = (line_offset > 0) ? line_offset - 1 : num_lines;
*top_fade_y_offset = y_offset - (float)line_height;
if (scroll_up)
{
*top_fade_alpha = fade_out_alpha;
*bottom_fade_alpha = fade_in_alpha;
}
else
{
*top_fade_alpha = fade_in_alpha;
*bottom_fade_alpha = fade_out_alpha;
}
*bottom_fade_line_offset = line_offset + num_display_lines;
*bottom_fade_y_offset = y_offset + (float)(line_height * num_display_lines);
}
static void xmb_animation_set_line_smooth_fade_parameters_default(
size_t *top_fade_line_offset, float *top_fade_y_offset, float *top_fade_alpha,
size_t *bottom_fade_line_offset, float *bottom_fade_y_offset, float *bottom_fade_alpha)
{
*top_fade_line_offset = 0;
*top_fade_y_offset = 0.0f;
*top_fade_alpha = 0.0f;
*bottom_fade_line_offset = 0;
*bottom_fade_y_offset = 0.0f;
*bottom_fade_alpha = 0.0f;
}
static void xmb_animation_line_ticker_smooth_generic(uint64_t idx,
bool fade_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,
bool *fade_active,
size_t *top_fade_line_offset, float *top_fade_y_offset,
float *top_fade_alpha,
size_t *bottom_fade_line_offset, float *bottom_fade_y_offset,
float *bottom_fade_alpha)
{
size_t scroll_ticks = TICKER_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;
if (phase >= scroll_ticks)
phase -= scroll_ticks;
else
{
/* Pause on first line */
pause = true;
phase = 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;
}
line_phase = phase % scroll_ticks;
if (pause)
{
/* Static display of max_display_lines
* (no animation) */
*num_display_lines = max_display_lines;
*y_offset = 0.0f;
*fade_active = false;
*line_offset = scroll_up ? 0 : excess_lines;
}
else if (line_phase == 0)
{
/* Static display of max_display_lines
* (no animation) */
*num_display_lines = max_display_lines;
*y_offset = 0.0f;
*fade_active = false;
*line_offset = scroll_up ? (phase / scroll_ticks) : (excess_lines - (phase / scroll_ticks));
}
else
{
/* Scroll animation is active */
*num_display_lines = (max_display_lines > 1) ? max_display_lines - 1 : 1;
*fade_active = fade_enabled;
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);
}
/* Set fade parameters if fade animation is active */
if (*fade_active)
xmb_animation_set_line_smooth_fade_parameters(
scroll_up, scroll_ticks, line_phase, line_height,
num_lines, *num_display_lines, *line_offset, *y_offset,
top_fade_line_offset, top_fade_y_offset, top_fade_alpha,
bottom_fade_line_offset, bottom_fade_y_offset, bottom_fade_alpha);
}
/* Set 'default' fade parameters if fade animation
* is inactive */
if (!*fade_active)
xmb_animation_set_line_smooth_fade_parameters_default(
top_fade_line_offset, top_fade_y_offset, top_fade_alpha,
bottom_fade_line_offset, bottom_fade_y_offset, bottom_fade_alpha);
}
static void xmb_animation_line_ticker_smooth_loop(uint64_t idx,
bool fade_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,
bool *fade_active,
size_t *top_fade_line_offset, float *top_fade_y_offset, float *top_fade_alpha,
size_t *bottom_fade_line_offset, float *bottom_fade_y_offset, float *bottom_fade_alpha)
{
size_t scroll_ticks = TICKER_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 (line_phase == (scroll_ticks - 1))
{
/* Static display of max_display_lines
* (no animation) */
*num_display_lines = max_display_lines;
*fade_active = false;
}
else
{
*num_display_lines = (max_display_lines > 1) ? max_display_lines - 1 : 1;
*fade_active = fade_enabled;
}
*y_offset = (float)line_height * (float)(scroll_ticks - line_phase) / (float)scroll_ticks;
/* Set fade parameters */
if (*fade_active)
xmb_animation_set_line_smooth_fade_parameters(
true, scroll_ticks, line_phase, line_height,
num_lines, *num_display_lines, *line_offset, *y_offset,
top_fade_line_offset, top_fade_y_offset, top_fade_alpha,
bottom_fade_line_offset, bottom_fade_y_offset, bottom_fade_alpha);
else
xmb_animation_set_line_smooth_fade_parameters_default(
top_fade_line_offset, top_fade_y_offset, top_fade_alpha,
bottom_fade_line_offset, bottom_fade_y_offset, bottom_fade_alpha);
}
XMB_NOINLINE static bool xmb_animation_line_ticker_smooth(gfx_animation_t *p_anim, gfx_animation_ctx_line_ticker_smooth_t *line_ticker)
{
char wrapped_str[PATH_MAX_LENGTH];
const char *wideglyph_str = NULL;
size_t line_ticker_src_len = 0;
size_t wrapped_str_len = 0;
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;
size_t top_fade_line_offset = 0;
size_t bottom_fade_line_offset = 0;
bool fade_active = false;
int wideglyph_width = 100;
#define XMB_LINE_TICKER_MAX_LINES 256
const char *line_starts[XMB_LINE_TICKER_MAX_LINES];
size_t line_lengths[XMB_LINE_TICKER_MAX_LINES];
size_t num_lines = 0;
size_t (*word_wrap_func)(char *dst, size_t dst_size,
const char *src, size_t src_len,
int line_width, int wideglyph_width, unsigned max_lines);
/* Sanity check */
if (!line_ticker)
return false;
if ( !line_ticker->font
|| (!line_ticker->src_str || !*line_ticker->src_str)
|| (line_ticker->field_width < 1)
|| (line_ticker->field_height < 1))
{
if (line_ticker->dst_str_len > 0)
line_ticker->dst_str[0] = '\0';
if (line_ticker->fade_enabled)
{
if (line_ticker->top_fade_str_len > 0)
line_ticker->top_fade_str[0] = '\0';
if (line_ticker->bottom_fade_str_len > 0)
line_ticker->bottom_fade_str[0] = '\0';
*line_ticker->top_fade_alpha = 0.0f;
*line_ticker->bottom_fade_alpha = 0.0f;
}
return false;
}
/* Get font dimensions */
if ((glyph_width = font_driver_get_message_width(
line_ticker->font, "a", 1, line_ticker->font_scale)) <= 0)
goto fail;
if ((wideglyph_str = msg_hash_get_wideglyph_str()))
{
int new_glyph_width = font_driver_get_message_width(
line_ticker->font, wideglyph_str, strlen(wideglyph_str),
line_ticker->font_scale);
if (new_glyph_width > 0)
wideglyph_width = new_glyph_width * 100 / glyph_width;
word_wrap_func = word_wrap_wideglyph;
}
else
word_wrap_func = word_wrap;
/* > Height */
if ((glyph_height = (int)roundf(line_ticker->font->metrics.height
* line_ticker->font_scale)) <= 0)
goto fail;
/* 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 fail;
/* Line wrap input string */
line_ticker_src_len = strlen(line_ticker->src_str);
wrapped_str_len = sizeof(wrapped_str);
if (line_ticker_src_len + 11 > wrapped_str_len)
goto fail;
wrapped_str[0] = '\0';
(word_wrap_func)(
wrapped_str,
wrapped_str_len,
line_ticker->src_str,
line_ticker_src_len,
(int)line_len,
wideglyph_width, 0);
if (!*wrapped_str)
goto fail;
/* Split into lines using memchr */
{
const char *p = wrapped_str;
const char *end_ptr = wrapped_str + strlen(wrapped_str);
while (p < end_ptr)
{
const char *nl = (const char*)memchr(p, '\n', end_ptr - p);
if (!nl)
nl = end_ptr;
if (num_lines >= XMB_LINE_TICKER_MAX_LINES)
break;
line_starts[num_lines] = p;
line_lengths[num_lines] = (size_t)(nl - p);
num_lines++;
p = nl + 1;
}
}
if (num_lines < 1)
goto fail;
/* Check whether total number of lines fits within
* the set field limit */
if (num_lines <= max_display_lines)
{
strlcpy(line_ticker->dst_str, wrapped_str, line_ticker->dst_str_len);
*line_ticker->y_offset = 0.0f;
if (line_ticker->fade_enabled)
{
if (line_ticker->top_fade_str_len > 0)
line_ticker->top_fade_str[0] = '\0';
if (line_ticker->bottom_fade_str_len > 0)
line_ticker->bottom_fade_str[0] = '\0';
*line_ticker->top_fade_y_offset = 0.0f;
*line_ticker->bottom_fade_y_offset = 0.0f;
*line_ticker->top_fade_alpha = 0.0f;
*line_ticker->bottom_fade_alpha = 0.0f;
}
return true;
}
/* Determine which lines should be shown, along with
* y axis draw offset */
switch (line_ticker->type_enum)
{
case TICKER_TYPE_LOOP:
xmb_animation_line_ticker_smooth_loop(
line_ticker->idx,
line_ticker->fade_enabled,
line_len, (size_t)glyph_height,
max_display_lines, num_lines,
&num_display_lines, &line_offset, line_ticker->y_offset,
&fade_active,
&top_fade_line_offset, line_ticker->top_fade_y_offset, line_ticker->top_fade_alpha,
&bottom_fade_line_offset, line_ticker->bottom_fade_y_offset, line_ticker->bottom_fade_alpha);
break;
case TICKER_TYPE_BOUNCE:
default:
xmb_animation_line_ticker_smooth_generic(
line_ticker->idx,
line_ticker->fade_enabled,
line_len, (size_t)glyph_height,
max_display_lines, num_lines,
&num_display_lines, &line_offset, line_ticker->y_offset,
&fade_active,
&top_fade_line_offset, line_ticker->top_fade_y_offset, line_ticker->top_fade_alpha,
&bottom_fade_line_offset, line_ticker->bottom_fade_y_offset, line_ticker->bottom_fade_alpha);
break;
}
if (!num_display_lines)
goto fail;
/* Build output string from required lines */
{
size_t i;
size_t dst_offset = 0;
bool is_loop = (line_ticker->type_enum == TICKER_TYPE_LOOP);
for (i = 0; i < num_display_lines; i++)
{
size_t line_idx = line_offset + i;
size_t copy_len;
/* Only wrap around for loop ticker mode;
* for bounce/generic mode, clamp to valid range */
if (is_loop)
line_idx %= num_lines;
else if (line_idx >= num_lines)
break;
copy_len = line_lengths[line_idx];
if (dst_offset + copy_len + 2 > line_ticker->dst_str_len)
break;
if (i > 0)
line_ticker->dst_str[dst_offset++] = '\n';
memcpy(line_ticker->dst_str + dst_offset,
line_starts[line_idx], copy_len);
dst_offset += copy_len;
}
if (dst_offset < line_ticker->dst_str_len)
line_ticker->dst_str[dst_offset] = '\0';
else if (line_ticker->dst_str_len > 0)
line_ticker->dst_str[line_ticker->dst_str_len - 1] = '\0';
}
/* Extract top/bottom fade strings, if required */
if (fade_active)
{
size_t top_fade_line_index = top_fade_line_offset;
size_t bottom_fade_line_index = bottom_fade_line_offset;
if (line_ticker->type_enum == TICKER_TYPE_LOOP)
{
/* For the top fade, use (num_lines + 1) so that the
* sentinel value 'num_lines' (set when line_offset == 0)
* correctly maps to >= num_lines and suppresses the
* fade string during the loop gap */
top_fade_line_index %= (num_lines + 1);
/* For the bottom fade, use num_lines so that the index
* wraps to the correct next line at the loop boundary
* instead of duplicating a line already in the main
* display */
bottom_fade_line_index %= num_lines;
}
if (top_fade_line_index < num_lines
&& line_ticker->top_fade_str_len > 0)
{
size_t copy_len = line_lengths[top_fade_line_index];
if (copy_len >= line_ticker->top_fade_str_len)
copy_len = line_ticker->top_fade_str_len - 1;
memcpy(line_ticker->top_fade_str,
line_starts[top_fade_line_index], copy_len);
line_ticker->top_fade_str[copy_len] = '\0';
}
if (bottom_fade_line_index < num_lines
&& line_ticker->bottom_fade_str_len > 0)
{
size_t copy_len = line_lengths[bottom_fade_line_index];
if (copy_len >= line_ticker->bottom_fade_str_len)
copy_len = line_ticker->bottom_fade_str_len - 1;
memcpy(line_ticker->bottom_fade_str,
line_starts[bottom_fade_line_index], copy_len);
line_ticker->bottom_fade_str[copy_len] = '\0';
}
}
p_anim->flags |= GFX_ANIM_FLAG_TICKER_IS_ACTIVE;
return true;
fail:
if (line_ticker->dst_str_len > 0)
line_ticker->dst_str[0] = '\0';
if (line_ticker->fade_enabled)
{
if (line_ticker->top_fade_str_len > 0)
line_ticker->top_fade_str[0] = '\0';
if (line_ticker->bottom_fade_str_len > 0)
line_ticker->bottom_fade_str[0] = '\0';
*line_ticker->top_fade_alpha = 0.0f;
*line_ticker->bottom_fade_alpha = 0.0f;
}
return false;
}
/* Loop-invariant state for xmb_draw_item().
*
* xmb_draw_item() used to take eighteen parameters, sixteen of which
* do not change across the visible-item loop in xmb_draw_items(). The
* x86-64 SysV ABI passes six arguments in registers, so twelve of them
* were written to the outgoing argument area by the caller and read
* back by the callee for every visible entry, every frame; the callee
* prologue then re-spilled several of them straight into its own frame.
*
* Bundling the invariants here means the caller fills one struct once
* per frame and passes a single pointer.
*
* Note that xmb_draw_items() was being handed the video dimensions
* twice -- as video_width/video_height and again as width/height --
* with identical values at the only call site. They are stored once. */
typedef struct
{
struct menu_state *menu_st;
void *userdata;
gfx_display_t *p_disp;
gfx_animation_t *p_anim;
gfx_display_ctx_driver_t *dispctx;
settings_t *settings;
math_matrix_4x4 *mymat;
xmb_handle_t *xmb;
xmb_node_t *core_node;
file_list_t *list;
float *color;
unsigned video_width;
unsigned video_height;
bool shadows_enable;
} xmb_draw_ctx_t;
/* Sublabel rendering for the selected entry.
*
* Split out of xmb_draw_item() rather than left inline: the block owns
* a 1024-byte sublabel buffer, two 256-byte fade buffers and two
* ticker structs, but only ever runs for one entry per frame (it is
* guarded on i == current). Inline, every visible entry paid for
* those slots in xmb_draw_item()'s frame, which is entered once per
* visible entry per frame on top of xmb_frame()'s own ~9.5 KB. */
XMB_NOINLINE static void xmb_draw_item_sublabel(
const xmb_draw_ctx_t *ctx,
const xmb_node_t *node,
const char *sublabel,
enum gfx_animation_ticker_type menu_ticker_type,
unsigned line_ticker_width,
bool use_smooth_ticker,
bool show_entry_icons)
{
xmb_handle_t *xmb = ctx->xmb;
settings_t *settings = ctx->settings;
bool shadows_enable = ctx->shadows_enable;
unsigned width = ctx->video_width;
unsigned height = ctx->video_height;
char entry_sublabel[MENU_LABEL_MAX_LENGTH];
char entry_sublabel_top_fade[MENU_LABEL_MAX_LENGTH >> 2];
char entry_sublabel_bottom_fade[MENU_LABEL_MAX_LENGTH >> 2];
gfx_animation_ctx_line_ticker_t line_ticker;
gfx_animation_ctx_line_ticker_smooth_t line_ticker_smooth;
float ticker_y_offset = 0.0f;
float ticker_top_fade_y_offset = 0.0f;
float ticker_bottom_fade_y_offset = 0.0f;
float ticker_top_fade_alpha = 0.0f;
float ticker_bottom_fade_alpha = 0.0f;
float sublabel_x = node->x + xmb->margins_screen_left +
xmb->icon_spacing_horizontal + xmb->margins_label_left;
float sublabel_y = xmb->margins_screen_top +
node->y + (xmb->margins_label_top * 3.5f);
entry_sublabel[0] = '\0';
entry_sublabel_top_fade[0] = '\0';
entry_sublabel_bottom_fade[0] = '\0';
if (!show_entry_icons)
sublabel_x -= xmb->icon_size;
if (use_smooth_ticker)
{
line_ticker_smooth.fade_enabled = true;
line_ticker_smooth.type_enum = menu_ticker_type;
line_ticker_smooth.idx = ctx->p_anim->ticker_pixel_line_idx;
line_ticker_smooth.font = xmb->font2;
line_ticker_smooth.font_scale = 1.0f;
line_ticker_smooth.field_width = (unsigned)(xmb->font2_size * 0.5f * line_ticker_width);
line_ticker_smooth.field_height = (unsigned)(
(xmb->icon_spacing_vertical * ((1 + xmb->under_item_offset) - xmb->active_item_factor)) -
(xmb->margins_label_top * 3.5f) - xmb->under_item_offset);
line_ticker_smooth.src_str = sublabel;
line_ticker_smooth.dst_str = entry_sublabel;
line_ticker_smooth.dst_str_len = sizeof(entry_sublabel);
line_ticker_smooth.y_offset = &ticker_y_offset;
line_ticker_smooth.top_fade_str = entry_sublabel_top_fade;
line_ticker_smooth.top_fade_str_len = sizeof(entry_sublabel_top_fade);
line_ticker_smooth.top_fade_y_offset = &ticker_top_fade_y_offset;
line_ticker_smooth.top_fade_alpha = &ticker_top_fade_alpha;
line_ticker_smooth.bottom_fade_str = entry_sublabel_bottom_fade;
line_ticker_smooth.bottom_fade_str_len = sizeof(entry_sublabel_bottom_fade);
line_ticker_smooth.bottom_fade_y_offset = &ticker_bottom_fade_y_offset;
line_ticker_smooth.bottom_fade_alpha = &ticker_bottom_fade_alpha;
xmb_animation_line_ticker_smooth(ctx->p_anim, &line_ticker_smooth);
}
else
{
line_ticker.type_enum = menu_ticker_type;
line_ticker.idx = ctx->p_anim->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 = sublabel;
xmb_animation_line_ticker(xmb, ctx->p_anim, &line_ticker);
}
/* Draw sublabel */
xmb_draw_text(shadows_enable, xmb, settings,
entry_sublabel,
sublabel_x,
ticker_y_offset + sublabel_y,
1, node->label_alpha * xmb->alpha_list, TEXT_ALIGN_LEFT,
width, height, xmb->font2);
/* Draw top/bottom line fade effect, if required */
if (use_smooth_ticker)
{
if ( *entry_sublabel_top_fade
&& ticker_top_fade_alpha > 0.0f)
xmb_draw_text(shadows_enable, xmb, settings,
entry_sublabel_top_fade,
sublabel_x, ticker_top_fade_y_offset + sublabel_y,
1, ticker_top_fade_alpha * node->label_alpha * xmb->alpha_list, TEXT_ALIGN_LEFT,
width, height, xmb->font2);
if ( *entry_sublabel_bottom_fade
&& ticker_bottom_fade_alpha > 0.0f)
xmb_draw_text(shadows_enable, xmb, settings,
entry_sublabel_bottom_fade,
sublabel_x, ticker_bottom_fade_y_offset + sublabel_y,
1, ticker_bottom_fade_alpha * node->label_alpha * xmb->alpha_list, TEXT_ALIGN_LEFT,
width, height, xmb->font2);
}
}
XMB_NOINLINE static int xmb_draw_item(
const xmb_draw_ctx_t *ctx,
size_t i,
size_t current)
{
/* Only the heavily-used members are hoisted into locals. The
* pointees are not const-qualified and the body makes over a
* hundred calls, so reading e.g. xmb through ctx at each of its 154
* use sites would force a reload after every one of them. The
* members used a handful of times are left as ctx-> so the register
* allocator is not handed sixteen values live across the whole
* function; measured, hoisting all of them costs more than it
* saves. */
settings_t *settings = ctx->settings;
xmb_handle_t *xmb = ctx->xmb;
file_list_t *list = ctx->list;
float *color = ctx->color;
unsigned video_width = ctx->video_width;
unsigned video_height = ctx->video_height;
unsigned width = ctx->video_width;
unsigned height = ctx->video_height;
bool shadows_enable = ctx->shadows_enable;
menu_entry_t entry;
float icon_x, icon_y, label_offset, gfx_icon_x, gfx_icon_y, gfx_icon_height, gfx_icon_width;
gfx_animation_ctx_ticker_t ticker;
gfx_animation_ctx_ticker_smooth_t ticker_smooth;
char tmp[NAME_MAX_LENGTH];
unsigned ticker_x_offset = 0;
const char *ticker_str = NULL;
unsigned entry_type = 0;
const float half_size = xmb->icon_size / 2.0f;
uintptr_t texture_switch = 0;
bool draw_text_value = false;
int extra_margins_setting_left = 0;
unsigned ticker_limit = ((xmb->use_ps3_layout) ? 37 : 37) * xmb->scale_mod[0];
unsigned line_ticker_width = ((xmb->use_ps3_layout) ? 58 : 58) * xmb->scale_mod[3];
xmb_node_t *node = (xmb_node_t*)list->list[i].userdata;
bool use_smooth_ticker = settings->bools.menu_ticker_smooth;
enum gfx_animation_ticker_type menu_ticker_type
= (enum gfx_animation_ticker_type)settings->uints.menu_ticker_type;
unsigned thumbnail_scale_factor = settings->uints.menu_xmb_thumbnail_scale_factor;
bool vertical_thumbnails = settings->bools.menu_xmb_vertical_thumbnails;
bool show_sublabels = settings->bools.menu_show_sublabels;
bool show_entry_icons = settings->bools.menu_xmb_entry_icons;
bool show_switch_icons = settings->bools.menu_xmb_switch_icons;
unsigned show_history_icons = settings->uints.playlist_show_history_icons;
unsigned vertical_fade_factor = settings->uints.menu_xmb_vertical_fade_factor;
bool show_icon_thumbnail = false;
/* Initial ticker configuration */
if (use_smooth_ticker)
{
ticker_smooth.idx = ctx->p_anim->ticker_pixel_idx;
ticker_smooth.font = xmb->font;
ticker_smooth.font_scale = 1.0f;
ticker_smooth.type_enum = menu_ticker_type;
ticker_smooth.spacer = NULL;
ticker_smooth.x_offset = &ticker_x_offset;
ticker_smooth.dst_str_width = NULL;
}
else
{
ticker.idx = ctx->p_anim->ticker_idx;
ticker.type_enum = menu_ticker_type;
ticker.spacer = NULL;
}
if (!node)
return 0;
tmp[0] = '\0';
icon_y = xmb->margins_screen_top + node->y + half_size;
if (icon_y < half_size)
return 0;
if (icon_y > height + xmb->icon_size)
return -1;
icon_x = node->x + xmb->margins_screen_left + xmb->icon_spacing_horizontal - half_size;
if (icon_x < -half_size || icon_x > width)
return 0;
MENU_ENTRY_INITIALIZE(entry);
entry.flags |= MENU_ENTRY_FLAG_PATH_ENABLED
| MENU_ENTRY_FLAG_LABEL_ENABLED
| MENU_ENTRY_FLAG_RICH_LABEL_ENABLED
| MENU_ENTRY_FLAG_VALUE_ENABLED;
if (i == current)
entry.flags |= MENU_ENTRY_FLAG_SUBLABEL_ENABLED;
menu_entry_get(&entry, 0, i, list, true);
entry_type = entry.type;
if (entry_type == FILE_TYPE_CONTENTLIST_ENTRY)
{
/* Sized to the only thing that can ever be copied in here.
* entry.path is char[256]; PATH_MAX_LENGTH is 2048, and this
* buffer sits in a frame that is entered once per visible entry
* per frame. */
char entry_path[sizeof(entry.path)];
strlcpy(entry_path, entry.path, sizeof(entry_path));
fill_pathname(entry_path, path_basename(entry_path), "",
sizeof(entry_path));
if (*entry_path)
strlcpy(entry.path, entry_path, sizeof(entry.path));
}
/* Only show switch icons with bool type options */
if (show_switch_icons && entry.setting_type == ST_BOOL)
{
if ( string_is_equal(entry.value, msg_hash_to_str(MENU_ENUM_LABEL_DISABLED))
|| string_is_equal(entry.value, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_OFF)))
{
if (xmb->textures.list[XMB_TEXTURE_SWITCH_OFF])
texture_switch = xmb->textures.list[XMB_TEXTURE_SWITCH_OFF];
else
draw_text_value = true;
}
else if (string_is_equal(entry.value, msg_hash_to_str(MENU_ENUM_LABEL_ENABLED))
|| string_is_equal(entry.value, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_ON)))
{
if (xmb->textures.list[XMB_TEXTURE_SWITCH_ON])
texture_switch = xmb->textures.list[XMB_TEXTURE_SWITCH_ON];
else
draw_text_value = true;
}
else
draw_text_value = true;
}
else
{
if (*entry.value)
{
bool found = false;
if (string_is_equal(entry.value, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MORE)))
found = (!show_entry_icons) ? false : true;
else if (string_starts_with_size(entry.value, "(", STRLEN_CONST("("))
&& string_ends_with(entry.value, ")"))
{
if (
string_is_equal(entry.value, "(PRESET)")
|| string_is_equal(entry.value, "(SHADER)")
|| string_is_equal(entry.value, "(COMP)")
|| string_is_equal(entry.value, "(CORE)")
|| string_is_equal(entry.value, "(MOVIE)")
|| string_is_equal(entry.value, "(MUSIC)")
|| string_is_equal(entry.value, "(DIR)")
|| string_is_equal(entry.value, "(RDB)")
|| string_is_equal(entry.value, "(CURSOR)")
|| string_is_equal(entry.value, "(CFILE)")
|| string_is_equal(entry.value, "(FILE)")
|| string_is_equal(entry.value, "(IMAGE)")
)
found = true;
}
if (!found)
draw_text_value = true;
}
else
draw_text_value = true;
}
if (!texture_switch)
{
bool show_right_thumbnail =
(gfx_thumbnail_is_enabled(ctx->menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
&& xmb->show_thumbnails
&& ( (xmb->thumbnails.right.status == GFX_THUMBNAIL_STATUS_AVAILABLE)
|| (xmb->thumbnails.right.status == GFX_THUMBNAIL_STATUS_PENDING)));
bool show_left_thumbnail =
(gfx_thumbnail_is_enabled(ctx->menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
&& xmb->show_thumbnails
&& ( (xmb->thumbnails.left.status == GFX_THUMBNAIL_STATUS_AVAILABLE)
|| (xmb->thumbnails.left.status == GFX_THUMBNAIL_STATUS_PENDING)));
if ( (xmb->is_playlist || xmb->is_explore_list)
&& !xmb->is_playlist_information
&& xmb->use_ps3_layout
&& ( show_right_thumbnail
|| (show_left_thumbnail && vertical_thumbnails))
)
{
ticker_limit = 37 * xmb->scale_mod[0];
/* Can increase text length if thumbnail is downscaled */
if (thumbnail_scale_factor < 100)
{
float ticker_scale_factor =
1.0f - ((float)thumbnail_scale_factor / 100.0f);
ticker_limit +=
(unsigned)(ticker_scale_factor * 20.0f * xmb->scale_mod[1]);
line_ticker_width +=
(unsigned)(ticker_scale_factor * 10.0f * xmb->scale_mod[3]);
}
}
else if (!draw_text_value || !*entry.value)
ticker_limit = ((xmb->use_ps3_layout) ? 75 : 65) * xmb->scale_mod[2];
}
switch (entry_type)
{
case FILE_TYPE_DOWNLOAD_CORE:
/* Special margin for 'installed' tag value */
extra_margins_setting_left = ((xmb->use_ps3_layout) ? 300 : 250) * xmb->last_scale_factor * xmb->scale_mod[2];
ticker_limit = ((xmb->use_ps3_layout) ? 80 : 58) * xmb->scale_mod[2];
break;
default:
if (!xmb->use_ps3_layout)
break;
if ((xmb->is_quick_menu && *xmb->savestate_thumbnail_file_path) || xmb->is_state_slot)
extra_margins_setting_left = -(100 * xmb->last_scale_factor * xmb->scale_mod[2]);
break;
}
if (!use_smooth_ticker && string_is_equal(settings->paths.path_menu_xmb_font, FILE_PATH_UNKNOWN))
{
ticker_limit *= 0.85f;
line_ticker_width *= 0.85f;
}
if (!show_entry_icons)
{
ticker_limit += xmb->icon_size / xmb->last_scale_factor / 16;
line_ticker_width += xmb->icon_size / xmb->last_scale_factor / 16;
}
if (xmb->scale_cap > 1.0f)
{
ticker_limit /= xmb->scale_cap;
line_ticker_width /= xmb->scale_cap;
}
/* Don't update ticker limit while waiting for thumbnail status */
if ( (xmb->is_playlist || xmb->is_explore_list)
&& ( ( gfx_thumbnail_is_enabled(ctx->menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
&& xmb->thumbnails.left.status == GFX_THUMBNAIL_STATUS_UNKNOWN)
|| ( gfx_thumbnail_is_enabled(ctx->menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
&& xmb->thumbnails.right.status == GFX_THUMBNAIL_STATUS_UNKNOWN))
)
; /* no-op */
else
xmb->ticker_limit = ticker_limit;
if (*entry.rich_label)
ticker_str = entry.rich_label;
else
ticker_str = entry.path;
if (use_smooth_ticker)
{
ticker_smooth.selected = (i == current);
ticker_smooth.field_width = xmb->font_size * 0.5f * xmb->ticker_limit;
ticker_smooth.src_str = ticker_str;
ticker_smooth.dst_str = tmp;
ticker_smooth.dst_str_len = sizeof(tmp);
if (ticker_smooth.src_str)
gfx_animation_ticker_smooth(&ticker_smooth);
}
else
{
ticker.selected = (i == current);
ticker.len = xmb->ticker_limit;
ticker.str = ticker_str;
ticker.s = tmp;
ticker.s_len = sizeof(tmp);
if (ticker.str)
gfx_animation_ticker(&ticker);
}
if (vertical_fade_factor)
{
float min_alpha = 0.01f;
float max_alpha = (i == current) ? xmb->items_active_alpha : xmb->items_passive_alpha;
float new_alpha = node->alpha;
float icon_space = xmb->icon_spacing_vertical;
float icon_ratio = icon_space / height / icon_space * 4;
float scr_margin = xmb->margins_screen_top + (icon_space / icon_ratio / 400);
float factor = vertical_fade_factor / 100.0f / icon_ratio;
if (!xmb->use_ps3_layout)
scr_margin -= (icon_space / 8);
/* Top */
if (i < current)
new_alpha = (node->y + scr_margin) / factor;
/* Bottom */
else if (i > current)
new_alpha = (height - node->y - scr_margin + icon_space) / factor;
/* Limits */
if (new_alpha < min_alpha)
new_alpha = min_alpha;
if (new_alpha > max_alpha)
new_alpha = max_alpha;
/* Avoid adjusting animating rows */
if (new_alpha < node->alpha || node->x == 0)
node->alpha = node->label_alpha = new_alpha;
}
label_offset = xmb->margins_label_top;
if (show_sublabels)
{
if ( i == current
&& width > 320 && height > 240
&& *entry.sublabel)
{
label_offset = -xmb->margins_label_top;
xmb_draw_item_sublabel(ctx, node, entry.sublabel,
menu_ticker_type, line_ticker_width,
use_smooth_ticker, show_entry_icons);
}
}
/* Draw entry index of current selection */
if ( i == current
&& xmb->entry_idx_enabled
&& *xmb->entry_index_str)
{
float entry_idx_margin = 12 * xmb->last_scale_factor;
float x_position = video_width - entry_idx_margin;
float y_position = video_height - entry_idx_margin;
xmb_draw_text(shadows_enable, xmb, settings,
xmb->entry_index_str, x_position, y_position,
1, vertical_thumbnails ? node->label_alpha : 1,
TEXT_ALIGN_RIGHT, width, height, xmb->font);
}
/* Entry label */
xmb_draw_text(shadows_enable, xmb, settings, tmp,
(float)ticker_x_offset
+ node->x
+ xmb->margins_screen_left
+ xmb->icon_spacing_horizontal
- (!show_entry_icons ? xmb->icon_size : 0)
+ xmb->margins_label_left,
xmb->margins_screen_top
+ node->y
+ label_offset,
1, node->label_alpha * xmb->alpha_list, TEXT_ALIGN_LEFT,
width, height, xmb->font);
/* Entry value */
tmp[0] = '\0';
if (use_smooth_ticker)
{
ticker_smooth.selected = (i == current);
ticker_smooth.field_width = xmb->font_size * 0.5f * ((xmb->use_ps3_layout) ? 40 : 26) * xmb->scale_mod[7];
ticker_smooth.src_str = entry.value;
ticker_smooth.dst_str = tmp;
ticker_smooth.dst_str_len = sizeof(tmp);
if (*entry.value)
gfx_animation_ticker_smooth(&ticker_smooth);
}
else
{
ticker.s = tmp;
ticker.s_len = sizeof(tmp);
ticker.len = ((xmb->use_ps3_layout) ? 40 : 26) * xmb->scale_mod[7];
ticker.selected = (i == current);
ticker.str = entry.value;
if (*entry.value)
gfx_animation_ticker(&ticker);
}
if (draw_text_value)
xmb_draw_text(shadows_enable, xmb, settings, tmp,
(float)ticker_x_offset
+ node->x
+ xmb->margins_screen_left
+ xmb->icon_spacing_horizontal
+ xmb->margins_label_left
+ xmb->margins_setting_left
+ extra_margins_setting_left,
xmb->margins_screen_top
+ node->y
+ xmb->margins_label_top,
1, node->label_alpha * xmb->alpha_list, TEXT_ALIGN_LEFT,
width, height, xmb->font);
gfx_display_set_alpha(color, MIN(node->alpha * xmb->alpha_list, xmb->alpha));
/* Entry icon */
if ( (!xmb->assets_missing)
&& (color[3] != 0)
&& show_entry_icons
&& ( (entry.flags & MENU_ENTRY_FLAG_CHECKED)
|| !( entry_type >= MENU_SETTING_DROPDOWN_ITEM
&& entry_type <= MENU_SETTING_DROPDOWN_SETTING_UINT_ITEM_SPECIAL))
)
{
uintptr_t texture = xmb_icon_get_id(xmb, ctx->core_node, node,
entry.enum_idx, entry.path, entry.label,
entry_type, (i == current), entry.flags & MENU_ENTRY_FLAG_CHECKED);
float scale_factor = node->zoom;
if (i != current)
{
/* Differentiate the basic setting icon from the rest */
if (texture == xmb->textures.list[XMB_TEXTURE_SUBSETTING])
gfx_display_set_alpha(color, MIN(node->alpha / 2.0f, xmb->alpha));
/* Highlight active icon more by dimming all passives */
else
gfx_display_set_alpha(color, MIN(node->alpha / 1.25f, xmb->alpha));
}
/* Playlist manager icons */
if (xmb->depth == 3 && entry.enum_idx == MENU_ENUM_LABEL_PLAYLIST_MANAGER_SETTINGS)
{
if (string_is_equal(entry.rich_label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_HISTORY_TAB)))
texture = xmb->textures.list[XMB_TEXTURE_HISTORY];
else if (string_is_equal(entry.rich_label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_FAVORITES_TAB)))
texture = xmb->textures.list[XMB_TEXTURE_FAVORITES];
#ifdef HAVE_IMAGEVIEWER
else if (string_is_equal(entry.rich_label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_IMAGES_TAB)))
texture = xmb->textures.list[XMB_TEXTURE_IMAGES];
#endif
else if (string_is_equal(entry.rich_label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MUSIC_TAB)))
texture = xmb->textures.list[XMB_TEXTURE_MUSICS];
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
else if (string_is_equal(entry.rich_label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_VIDEO_TAB)))
texture = xmb->textures.list[XMB_TEXTURE_MOVIES];
#endif
else if (i < xmb->horizontal_list.size)
{
xmb_node_t *sidebar_node = NULL;
unsigned offset = 0;
/* Ignore Explore Views */
for (offset = 0; offset < xmb->horizontal_list.size; offset++)
{
char playlist_file_noext[NAME_MAX_LENGTH];
fill_pathname(playlist_file_noext, xmb->horizontal_list.list[offset].path, "",
sizeof(playlist_file_noext));
if (string_is_equal(playlist_file_noext, entry.rich_label))
break;
}
sidebar_node = (xmb_node_t*)file_list_get_userdata_at_offset(&xmb->horizontal_list, offset);
if (sidebar_node && sidebar_node->icon)
texture = sidebar_node->icon;
}
}
/* "Main Menu" playlists */
else if (xmb->depth == 2 && entry_type == FILE_TYPE_PLAYLIST_COLLECTION)
{
xmb_node_t *sidebar_node = NULL;
unsigned offset = list->list[i].entry_idx;
/* Search for sorted icon order */
if (settings->bools.ozone_sort_after_truncate_playlist_name)
{
for (offset = 0; offset < xmb->horizontal_list.size; offset++)
{
char playlist_file_noext[NAME_MAX_LENGTH];
fill_pathname(playlist_file_noext,
xmb->horizontal_list.list[offset].path, "",
sizeof(playlist_file_noext));
if (string_is_equal(playlist_file_noext, entry.rich_label))
break;
}
}
sidebar_node = (xmb_node_t*)
(xmb->horizontal_list.size)
? (xmb_node_t*)file_list_get_userdata_at_offset(&xmb->horizontal_list, offset)
: NULL;
if (sidebar_node && sidebar_node->icon)
texture = sidebar_node->icon;
}
/* History/Favorite console specific content icons */
else if (entry_type == FILE_TYPE_RPL_ENTRY
&& show_history_icons != PLAYLIST_SHOW_HISTORY_ICONS_DEFAULT)
{
switch (xmb_get_system_tab(xmb, (unsigned)xmb->categories_selection_ptr))
{
case XMB_SYSTEM_TAB_MAIN:
{
/* Special fall-through for "Main Menu" > Playlists > History/Favorites */
char title[NAME_MAX_LENGTH];
bool is_history = false;
menu_entries_get_title(title, sizeof(title));
is_history =
string_is_equal(title, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_LOAD_CONTENT_HISTORY))
|| string_is_equal(title, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_GOTO_FAVORITES));
/* Reset unknown file icon */
if (is_history || xmb->is_quick_menu)
texture = xmb->textures.list[XMB_TEXTURE_FILE];
/* Force "Main Menu" playlists to use content icon
* inside the playlist regardless of the option,
* since playlists themselves use the main icon */
if (!is_history && (xmb->is_quick_menu || xmb->depth > 2))
show_history_icons = PLAYLIST_SHOW_HISTORY_ICONS_CONTENT;
}
case XMB_SYSTEM_TAB_HISTORY:
case XMB_SYSTEM_TAB_FAVORITES:
{
const struct playlist_entry *pl_entry = NULL;
xmb_node_t *db_node = NULL;
playlist_get_index(playlist_get_cached(), entry.entry_idx, &pl_entry);
if ( pl_entry
&& (pl_entry->db_name && *pl_entry->db_name)
&& (db_node = RHMAP_GET_STR(xmb->playlist_db_node_map, pl_entry->db_name)))
{
switch (show_history_icons)
{
case PLAYLIST_SHOW_HISTORY_ICONS_MAIN:
texture = db_node->icon;
break;
case PLAYLIST_SHOW_HISTORY_ICONS_CONTENT:
texture = db_node->content_icon;
break;
default:
break;
}
}
}
break;
default:
break;
}
}
gfx_icon_x = icon_x;
gfx_icon_y = icon_y;
gfx_icon_height = gfx_icon_width = xmb->icon_size;
/* node->thumbnail_icon is tested first and deliberately: it is in
* the node's hot cacheline, whereas everything behind it is a
* separate ~15 KB allocation. Entries that never had an icon
* resolved -- every entry outside a playlist, and every entry
* when icon thumbnails are off -- now cost no second line fetch
* here at all. */
show_icon_thumbnail =
xmb->is_playlist
&& node->thumbnail_icon
&& gfx_thumbnail_is_enabled(&node->thumbnail_icon->thumbnail_path_data, GFX_THUMBNAIL_ICON)
&& node->thumbnail_icon->icon.status == GFX_THUMBNAIL_STATUS_AVAILABLE;
if (show_icon_thumbnail)
{
float gfx_icon_width_draw, gfx_icon_height_draw, gfx_icon_aspect;
float gfx_icon_x_end = icon_x + xmb->icon_size;
float zoom_mp = (xmb->use_ps3_layout) ? 2.0f : 1.0f;
bool zoom = (i == current);
texture = node->thumbnail_icon->icon.texture;
/* Show selected item as "current menu" icon instead */
if (zoom)
gfx_display_set_alpha(color, 0);
/* More active zoom */
if (zoom)
gfx_icon_height = (xmb->icon_size * zoom_mp);
/* More width room for non-squares */
gfx_icon_width = (gfx_icon_height * 1.5f);
gfx_icon_aspect = (gfx_icon_width / gfx_icon_height);
gfx_thumbnail_get_draw_dimensions(
&node->thumbnail_icon->icon,
gfx_icon_width, gfx_icon_height, 1.0f,
&gfx_icon_width_draw, &gfx_icon_height_draw);
gfx_icon_width_draw = ceil(gfx_icon_width_draw);
gfx_icon_height_draw = ceil(gfx_icon_height_draw);
/* Match center with normal icons */
icon_x -= (gfx_icon_width / 6.0f);
if (xmb->use_ps3_layout)
{
/* Align current to right instead of center */
if (zoom)
icon_x -= (gfx_icon_width_draw / 2.0f) + (gfx_icon_width / 24.0f);
/* Center alignments */
gfx_icon_x = icon_x + (gfx_icon_width - gfx_icon_width_draw) / 2.0f;
/* Limit current maximum right aligment */
if (zoom && gfx_icon_x + gfx_icon_width_draw > gfx_icon_x_end)
gfx_icon_x -= (gfx_icon_x_end - gfx_icon_width_draw - gfx_icon_x);
}
else
{
float icon_x_orig = gfx_icon_x;
/* Center alignments */
gfx_icon_x = icon_x + (gfx_icon_width - gfx_icon_width_draw) / 2.0f;
/* Limit current maximum left alignment */
if (zoom && gfx_icon_x < icon_x_orig)
gfx_icon_x -= gfx_icon_x - icon_x_orig;
/* Limit current maximum right aligment */
if (zoom && gfx_icon_x + gfx_icon_width_draw > gfx_icon_x_end)
{
gfx_icon_width_draw = gfx_icon_x_end - gfx_icon_x;
gfx_icon_height_draw = gfx_icon_width_draw / gfx_icon_aspect;
}
}
gfx_icon_y = icon_y - (gfx_icon_height - gfx_icon_height_draw) / 2.0f;
if (zoom && xmb->use_ps3_layout)
gfx_icon_y += (xmb->icon_size / 1.5f);
gfx_icon_width = gfx_icon_width_draw;
gfx_icon_height = gfx_icon_height_draw;
}
xmb_draw_icon(
ctx->userdata,
ctx->p_disp,
ctx->dispctx,
video_width,
video_height,
(show_icon_thumbnail) ? false : shadows_enable,
gfx_icon_width,
gfx_icon_height,
texture,
gfx_icon_x,
gfx_icon_y,
width,
height,
xmb->alpha,
0,
scale_factor,
&color[0],
xmb->shadow_offset / 2,
ctx->mymat);
}
/* Switch icon ON/OFF */
if ( !xmb->assets_missing
&& texture_switch != 0
&& color[3] != 0)
{
if (texture_switch == xmb->textures.list[XMB_TEXTURE_SWITCH_OFF])
gfx_display_set_alpha(color, MIN(node->alpha / 2, xmb->alpha));
else
gfx_display_set_alpha(color, MIN(node->alpha, xmb->alpha));
xmb_draw_icon(
ctx->userdata,
ctx->p_disp,
ctx->dispctx,
video_width,
video_height,
shadows_enable,
xmb->icon_size,
xmb->icon_size,
texture_switch,
node->x
+ xmb->margins_screen_left
+ xmb->icon_spacing_horizontal
+ xmb->icon_size / 2.0
+ xmb->margins_setting_left,
xmb->margins_screen_top + node->y + xmb->icon_size / 2.0,
width, height,
node->alpha,
0,
1,
&color[0],
xmb->shadow_offset,
ctx->mymat);
}
/* Current selection indicator arrow for compact mode */
if (!show_entry_icons)
{
uintptr_t tex = (
!xmb->assets_missing
&& i == current
&& settings->uints.menu_xmb_current_menu_icon != XMB_CURRENT_MENU_ICON_NORMAL)
? xmb->textures.list[XMB_TEXTURE_ARROW] : 0;
int icon_size = xmb->icon_size;
int current_x = xmb->margins_screen_left + (icon_size / 3.0f);
int current_y = xmb->margins_screen_top
+ (icon_size / 2.0f)
+ (xmb->icon_spacing_vertical * xmb->active_item_factor);
gfx_display_set_alpha(color, MIN(xmb->items_passive_alpha, xmb->alpha));
/* Checked dropdown indicator replaces arrow */
if (!xmb->assets_missing && entry.flags & MENU_ENTRY_FLAG_CHECKED)
{
current_y = icon_y;
tex = xmb->textures.list[XMB_TEXTURE_CHECKMARK];
}
if (tex)
{
xmb_draw_icon(
ctx->userdata,
ctx->p_disp,
ctx->dispctx,
video_width,
video_height,
shadows_enable,
icon_size,
icon_size,
tex,
current_x,
current_y,
video_width,
video_height,
node->alpha,
(entry.flags & MENU_ENTRY_FLAG_CHECKED) ? 0 : M_PI,
0.333f,
&color[0],
xmb->shadow_offset / 2,
ctx->mymat);
}
else if (xmb->assets_missing)
{
/* RGUI-like missing assets fallback for current and checked items */
if (i == current)
xmb_draw_text(shadows_enable, xmb, settings,
">",
xmb->margins_screen_left + xmb->margins_label_left,
xmb->margins_screen_top + node->y + label_offset,
1,
node->label_alpha * xmb->alpha_list,
TEXT_ALIGN_LEFT,
video_width,
video_height,
xmb->font);
if (entry.flags & MENU_ENTRY_FLAG_CHECKED)
xmb_draw_text(shadows_enable, xmb, settings,
"|",
xmb->margins_screen_left + (xmb->margins_label_left * 1.33f),
xmb->margins_screen_top + node->y + label_offset,
1,
node->label_alpha * xmb->alpha_list,
TEXT_ALIGN_LEFT,
video_width,
video_height,
xmb->font);
}
}
return 0;
}
static void xmb_draw_items(
void *userdata,
gfx_display_t *p_disp,
gfx_display_ctx_driver_t *dispctx,
gfx_animation_t *p_anim,
struct menu_state *menu_st,
settings_t *settings,
unsigned video_width,
unsigned video_height,
bool shadows_enable,
xmb_handle_t *xmb,
file_list_t *list,
size_t current,
size_t cat_selection_ptr,
float *color,
math_matrix_4x4 *mymat)
{
xmb_draw_ctx_t ctx;
size_t i;
unsigned first, last;
size_t end = 0;
if (!list || !list->size)
return;
end = list->size;
i = menu_st->entries.begin;
first = (unsigned)i;
last = (unsigned)(end - 1);
xmb_calculate_visible_range(xmb, video_height, end, (unsigned)current, &first, &last);
ctx.menu_st = menu_st;
ctx.userdata = userdata;
ctx.p_disp = p_disp;
ctx.p_anim = p_anim;
ctx.dispctx = dispctx;
ctx.settings = settings;
ctx.mymat = mymat;
ctx.xmb = xmb;
ctx.core_node = NULL;
ctx.list = list;
ctx.color = color;
ctx.video_width = video_width;
ctx.video_height = video_height;
ctx.shadows_enable = shadows_enable;
if (cat_selection_ptr > xmb->system_tab_end)
ctx.core_node = xmb_get_userdata_from_horizontal_list(
xmb, (unsigned)(cat_selection_ptr - (xmb->system_tab_end + 1)));
for (i = first; i <= last; i++)
{
if (xmb_draw_item(&ctx, i, current) == -1)
break;
}
}
static INLINE bool xmb_use_ps3_layout(unsigned menu_xmb_layout,
unsigned width, unsigned height)
{
switch (menu_xmb_layout)
{
case 1: /* PS3 */
return true;
case 2: /* PSP */
return false;
case 0:
default:
break;
}
/* Automatic
* > Use PSP layout on tiny screens */
return (width > 320) && (height > 240);
}
static INLINE float xmb_get_scale_factor(float menu_scale_factor,
bool use_ps3_layout, unsigned width)
{
float scale_factor;
/* PS3 Layout */
if (use_ps3_layout)
scale_factor = ((menu_scale_factor * (float)width) / 1920.0f);
/* PSP Layout */
else
{
#ifdef _3DS
scale_factor = menu_scale_factor / 4.0f;
#else
scale_factor = ((menu_scale_factor * (float)width) / 1920.0f) * 1.5f;
#endif
}
/* Apply safety limit */
if (scale_factor < 0.1f)
return 0.1f;
return scale_factor;
}
static void xmb_set_thumbnail_delay(bool on)
{
if (on)
{
gfx_thumbnail_set_stream_delay(-1.0f);
gfx_thumbnail_set_fade_duration(-1.0f);
}
else
{
gfx_thumbnail_set_stream_delay(0);
gfx_thumbnail_set_fade_duration(1);
}
}
/* Disables the fullscreen thumbnail view, with
* an optional fade out animation */
static void xmb_hide_fullscreen_thumbnails(
xmb_handle_t *xmb, bool animate)
{
uintptr_t alpha_tag = (uintptr_t)&xmb->fullscreen_thumbnail_alpha;
/* Kill any existing fade in/out animations */
gfx_animation_kill_by_tag(&alpha_tag);
/* Check whether animations are enabled */
if (animate && (xmb->fullscreen_thumbnail_alpha > 0.0f))
{
gfx_animation_ctx_entry_t animation_entry;
/* Configure fade out animation */
animation_entry.easing_enum = XMB_EASING_ALPHA;
animation_entry.tag = alpha_tag;
animation_entry.duration = gfx_thumb_get_ptr()->fade_duration;
animation_entry.target_value = 0.0f;
animation_entry.subject = &xmb->fullscreen_thumbnail_alpha;
animation_entry.cb = NULL;
animation_entry.userdata = NULL;
/* Push animation */
gfx_animation_push(&animation_entry);
}
/* No animation - just set thumbnail alpha to zero */
else
xmb->fullscreen_thumbnail_alpha = 0.0f;
/* Disable fullscreen thumbnails */
xmb->show_fullscreen_thumbnails = false;
xmb->want_fullscreen_thumbnails = false;
xmb_set_thumbnail_delay(true);
}
/* Enables (and triggers a fade in of) the fullscreen
* thumbnail view */
static void xmb_show_fullscreen_thumbnails(
xmb_handle_t *xmb, struct menu_state *menu_st,
size_t selection)
{
gfx_animation_ctx_entry_t animation_entry;
bool animate = !xmb->want_fullscreen_thumbnails;
uintptr_t alpha_tag = (uintptr_t)&xmb->fullscreen_thumbnail_alpha;
/* Cache selected entry label
* (used as title when fullscreen thumbnails
* are shown) */
if (menu_update_fullscreen_thumbnail_label(xmb->fullscreen_thumbnail_label,
sizeof(xmb->fullscreen_thumbnail_label),
xmb->is_quick_menu,
xmb->title_name) == 0)
xmb->fullscreen_thumbnail_label[0] = '\0';
if (animate)
{
/* Configure fade in animation */
animation_entry.easing_enum = XMB_EASING_ALPHA;
animation_entry.tag = alpha_tag;
animation_entry.duration = gfx_thumb_get_ptr()->fade_duration;
animation_entry.target_value = 1.0f;
animation_entry.subject = &xmb->fullscreen_thumbnail_alpha;
animation_entry.cb = NULL;
animation_entry.userdata = NULL;
/* Push animation */
gfx_animation_push(&animation_entry);
}
else
xmb->fullscreen_thumbnail_alpha = 1.0f;
/* Enable fullscreen thumbnails */
xmb->fullscreen_thumbnail_selection = selection;
xmb->show_fullscreen_thumbnails = true;
xmb->want_fullscreen_thumbnails = true;
xmb_set_thumbnail_delay(false);
}
static INLINE bool xmb_fullscreen_thumbnails_available(xmb_handle_t *xmb,
struct menu_state *menu_st)
{
bool ret = xmb->fullscreen_thumbnails_available
&& ( gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT));
if (xmb->is_state_slot)
ret = true;
return ret;
}
static enum menu_action xmb_parse_menu_entry_action(
xmb_handle_t *xmb, enum menu_action action)
{
enum menu_action new_action = action;
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
/* Scan user inputs */
switch (action)
{
case MENU_ACTION_LEFT:
case MENU_ACTION_RIGHT:
if (xmb->show_fullscreen_thumbnails && (xmb->is_quick_menu && !menu_is_running_quick_menu()))
return MENU_ACTION_NOOP;
/* Check whether left/right action will
* trigger a tab switch event */
if (xmb->depth == 1)
{
retro_time_t current_time = menu_driver_get_current_time();
/* Determine whether input repeat is
* currently active
* > This is always true when scroll
* acceleration is greater than zero */
size_t scroll_accel = menu_st->scroll.acceleration;
#ifdef HAVE_AUDIOMIXER
settings_t *settings = config_get_ptr();
size_t category = xmb->categories_selection_ptr;
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL) + xmb->system_tab_end;
/* We only want the scrolling sound to play if any of the following are true:
* 1. Wraparound is enabled (since the category is guaranteed to change)
* 2. We're scrolling right, but we aren't on the last category
* 3. We're scrolling left, but we aren't on the first category */
bool fail_condition = ((action == MENU_ACTION_RIGHT)
? (category == list_size)
: (category == 0)) && !(settings->bools.menu_navigation_wraparound_enable);
if ( ((current_time - xmb->last_tab_switch_time) >= XMB_TAB_SWITCH_REPEAT_DELAY || scroll_accel <= 0)
&& !fail_condition)
audio_driver_mixer_play_scroll_sound(action == MENU_ACTION_RIGHT);
#endif
if (scroll_accel > 0)
{
/* Ignore input action if tab switch period
* is less than defined limit */
if ((current_time - xmb->last_tab_switch_time) < XMB_TAB_SWITCH_REPEAT_DELAY)
{
new_action = MENU_ACTION_NOOP;
break;
}
}
xmb->last_tab_switch_time = current_time;
}
break;
case MENU_ACTION_START:
if ( (xmb->is_state_slot)
|| (xmb->is_quick_menu && menu_is_running_quick_menu()))
break;
/* If this is a menu with thumbnails, attempt
* to show fullscreen thumbnail view */
if ( xmb_fullscreen_thumbnails_available(xmb, menu_st)
&& !xmb->show_fullscreen_thumbnails)
{
xmb_hide_fullscreen_thumbnails(xmb, false);
xmb_show_fullscreen_thumbnails(xmb, menu_st, menu_st->selection_ptr);
new_action = MENU_ACTION_NOOP;
}
else if (xmb->show_fullscreen_thumbnails
|| xmb->want_fullscreen_thumbnails)
{
xmb_hide_fullscreen_thumbnails(xmb, true);
new_action = MENU_ACTION_NOOP;
}
break;
case MENU_ACTION_SCAN:
if (xmb->is_playlist_tab)
{
size_t selection_total = menu_st->entries.list ? MENU_LIST_GET_SELECTION(menu_st->entries.list, 0)->size : 0;
size_t selection = menu_st->selection_ptr;
size_t new_selection = random_range(0, (unsigned)(selection_total - 1));
menu_entry_t entry_new;
MENU_ENTRY_INITIALIZE(entry_new);
menu_entry_get(&entry_new, 0, new_selection, NULL, false);
/* Keep randomizing until selection is a fresh playlist */
while (new_selection == selection || entry_new.type != FILE_TYPE_PLAYLIST_COLLECTION)
{
new_selection = random_range(0, (unsigned)(selection_total - 1));
menu_entry_get(&entry_new, 0, new_selection, NULL, false);
}
if (new_selection != selection)
{
menu_st->selection_ptr = new_selection;
if (menu_st->driver_ctx->navigation_set)
menu_st->driver_ctx->navigation_set(menu_st->userdata, true);
}
new_action = MENU_ACTION_NOOP;
#ifdef HAVE_AUDIOMIXER
if (new_selection != selection)
audio_driver_mixer_play_scroll_sound(true);
#endif
}
else if ((xmb->is_playlist)
|| (xmb->is_explore_list))
{
size_t selection = menu_st->selection_ptr;
size_t new_selection = menu_playlist_random_selection(selection, xmb->is_explore_list);
if (new_selection != selection)
{
menu_st->selection_ptr = new_selection;
xmb_selection_pointer_changed(xmb, true);
}
new_action = MENU_ACTION_NOOP;
#ifdef HAVE_AUDIOMIXER
if (new_selection != selection)
audio_driver_mixer_play_scroll_sound(true);
#endif
break;
}
if ( !xmb->show_fullscreen_thumbnails
&& ( (xmb->is_state_slot)
|| (xmb->is_quick_menu && *xmb->savestate_thumbnail_file_path)))
{
xmb_hide_fullscreen_thumbnails(xmb, false);
xmb_show_fullscreen_thumbnails(xmb, menu_st, menu_st->selection_ptr);
new_action = MENU_ACTION_NOOP;
}
else if (xmb->show_fullscreen_thumbnails
&& (xmb->is_state_slot || (xmb->is_quick_menu && menu_is_running_quick_menu())))
{
xmb_hide_fullscreen_thumbnails(xmb, true);
new_action = MENU_ACTION_NOOP;
}
break;
case MENU_ACTION_SEARCH:
/* Playlist thumbnail cycle */
if ( (xmb->show_fullscreen_thumbnails)
|| ((xmb->is_quick_menu) && !menu_is_running_quick_menu()))
{
xmb->skip_thumbnail_reset = false;
action_switch_thumbnail(NULL, NULL, 0, 0);
new_action = MENU_ACTION_NOOP;
}
break;
case MENU_ACTION_OK:
if (xmb->is_state_slot)
xmb->skip_thumbnail_reset = true;
/* Open fullscreen thumbnail with Ok when core is running
to prevent accidental imageviewer core launch */
if ( xmb->libretro_running
&& xmb->is_file_list
&& xmb_fullscreen_thumbnails_available(xmb, menu_st)
&& xmb_get_system_tab(xmb, (unsigned)xmb->categories_selection_ptr) == XMB_SYSTEM_TAB_MAIN)
{
/* Allow launch if already using "imageviewer" core */
if (string_is_equal(runloop_state_get_ptr()->system.info.library_name, "Image Viewer"))
break;
if (xmb->show_fullscreen_thumbnails)
xmb_hide_fullscreen_thumbnails(xmb, true);
else
xmb_show_fullscreen_thumbnails(xmb, menu_st, menu_st->selection_ptr);
return MENU_ACTION_NOOP;
}
if (xmb->show_fullscreen_thumbnails || xmb->want_fullscreen_thumbnails)
{
xmb_hide_fullscreen_thumbnails(xmb, true);
if (!xmb->is_state_slot && !xmb->is_playlist && !xmb->is_explore_list)
return MENU_ACTION_NOOP;
}
/* Make transition smoother for single-click playlist launching */
if ( config_get_ptr()->bools.input_menu_singleclick_playlists
&& (xmb->is_playlist || xmb->is_explore_list))
{
if (xmb->is_explore_list)
{
#if defined(HAVE_LIBRETRODB)
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
menu_entry_get(&entry, 0, menu_st->selection_ptr, NULL, true);
if ( entry.type == FILE_TYPE_RDB
|| entry.type == FILE_TYPE_PLAIN
|| !menu_explore_is_content_list())
break;
#endif
}
xmb->alpha_list = 0.0f;
xmb->draw_entry_delay = MENU_DRAW_ENTRY_DELAY;
}
break;
case MENU_ACTION_CANCEL:
if (xmb->is_state_slot)
xmb->skip_thumbnail_reset = true;
if (xmb->show_fullscreen_thumbnails || xmb->want_fullscreen_thumbnails)
{
xmb->skip_thumbnail_reset = false;
xmb_hide_fullscreen_thumbnails(xmb, true);
return MENU_ACTION_NOOP;
}
/* Back up to Main Menu and first item */
if ( !menu_entries_search_get_terms()
&& MENU_LIST_GET_STACK_SIZE(menu_list, 0) == 1)
{
if (xmb_get_system_tab(xmb,
(unsigned)xmb->categories_selection_ptr) == XMB_SYSTEM_TAB_MAIN)
{
/* Jump to first item on Main Menu */
menu_st->selection_ptr = 0;
xmb_selection_pointer_changed(xmb, true);
}
else if (!config_get_ptr()->bools.menu_xmb_show_horizontal_list)
{
/* Reset horizontal list to Main Menu */
file_list_t *menu_stack = menu_list ? MENU_LIST_GET(menu_list, 0) : NULL;
file_list_t *selection_buf = menu_list ? MENU_LIST_GET_SELECTION(menu_list, 0) : NULL;
size_t stack_size = menu_stack ? menu_stack->size : 0;
/* Relabelling needs an entry to relabel: with an empty
* stack, stack_size - 1 indexes SIZE_MAX and the free
* releases whatever that read returns. */
if (stack_size > 0)
{
if (menu_stack->list[stack_size - 1].label)
free(menu_stack->list[stack_size - 1].label);
menu_stack->list[stack_size - 1].label = NULL;
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_MAIN_MENU_STR);
menu_stack->list[stack_size - 1].type = MENU_SETTINGS;
}
menu_driver_deferred_push_content_list(selection_buf);
}
else if (config_get_ptr()->bools.input_menu_allow_tabs_back)
{
/* Jump to Main Menu */
size_t i = 0;
size_t current_tab = xmb->categories_selection_ptr;
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
menu_entry_get(&entry, 0, menu_st->selection_ptr, NULL, true);
/* Icon animations get stuck if they happen too fast,
* therefore allow it only on the last action */
xmb->allow_horizontal_animation = false;
/* Prevent dynamic wallpaper flashing during jump */
xmb->allow_dynamic_wallpaper = false;
for (i = 0; i < current_tab; i++)
{
if (i == current_tab - 1)
{
xmb->allow_horizontal_animation = true;
xmb->allow_dynamic_wallpaper = true;
}
xmb_menu_entry_action(xmb,
&entry, menu_st->selection_ptr, MENU_ACTION_LEFT);
}
}
return MENU_ACTION_NOOP;
}
break;
case MENU_ACTION_INFO:
if ( (xmb->is_playlist)
|| (xmb->is_explore_list))
{
xmb->show_thumbnails = !xmb->show_thumbnails;
new_action = MENU_ACTION_NOOP;
}
break;
case MENU_ACTION_UP:
case MENU_ACTION_DOWN:
case MENU_ACTION_SCROLL_UP:
case MENU_ACTION_SCROLL_DOWN:
if (xmb->show_fullscreen_thumbnails && xmb->is_quick_menu)
return MENU_ACTION_NOOP;
break;
default:
/* In all other cases, pass through input
* menu action without intervention */
break;
}
switch (new_action)
{
case MENU_ACTION_UP:
case MENU_ACTION_DOWN:
case MENU_ACTION_LEFT:
case MENU_ACTION_RIGHT:
case MENU_ACTION_CANCEL:
if ( config_get_ptr()->bools.input_menu_singleclick_playlists
&& xmb->alpha_list < 1.0f)
{
xmb_animation_list_alpha(xmb, true);
new_action = MENU_ACTION_NOOP;
}
break;
default:
break;
}
return new_action;
}
/* Menu entry action callback */
static int xmb_menu_entry_action(
void *userdata, menu_entry_t *entry,
size_t i, enum menu_action action)
{
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
/* Process input action */
enum menu_action new_action = xmb_parse_menu_entry_action(xmb, action);
/* Call standard generic_menu_entry_action() function */
return generic_menu_entry_action(userdata, entry, i, new_action);
}
static void xmb_layout_common(xmb_handle_t *xmb, float scale_factor, unsigned new_font_size)
{
float margins_title = xmb->margins_title;
float margins_title_h_offset = xmb->margins_title_horizontal_offset;
xmb->categories_active_zoom = 1.0f;
xmb->categories_passive_zoom = 0.5f;
xmb->items_active_zoom = 1.0f;
xmb->items_passive_zoom = 0.5f;
xmb->categories_active_alpha = 1.0f;
xmb->categories_passive_alpha = 0.75f;
xmb->items_active_alpha = 1.0f;
xmb->items_passive_alpha = 0.75f;
xmb->shadow_offset = 4.0f * scale_factor;
if (xmb->shadow_offset < 1.0)
xmb->shadow_offset = 1.0f;
if (xmb->shadow_offset > 2.0)
xmb->shadow_offset = 2.0f;
xmb->margins_title_left = (margins_title * scale_factor)
+ (4 * scale_factor)
+ (margins_title_h_offset * scale_factor);
xmb->margins_title_top = (margins_title * scale_factor)
+ (new_font_size - (new_font_size / 6) * scale_factor);
xmb->margins_title_bottom = (margins_title * scale_factor) + (4 * scale_factor);
xmb->margins_dialog = new_font_size * 2.0f;
xmb->margins_slice = new_font_size / 2.0f;
xmb->cursor_size = 64.0f * scale_factor;
xmb->icon_size = 128.0f * scale_factor;
/* Limit minimum font size */
xmb->font_size = (xmb->font_size < 7) ? 7.0f : xmb->font_size;
xmb->font2_size = (xmb->font2_size < 6) ? 6.0f : xmb->font2_size;
}
static void xmb_layout_ps3(xmb_handle_t *xmb, settings_t *settings)
{
float scale_factor = xmb->last_scale_factor;
float scale_cap = xmb->scale_cap;
unsigned new_font_size = 32 * scale_factor;
xmb->above_subitem_offset = 1.5f / scale_cap;
xmb->above_item_offset = -1.0f / scale_cap;
xmb->active_item_factor = 3.0f / scale_cap;
xmb->under_item_offset = 5.0f / scale_cap;
/* Scaling limits */
if (xmb->active_item_factor < 2)
xmb->active_item_factor = 2.0f;
if (xmb->above_subitem_offset < 1.5f)
xmb->above_subitem_offset = 1.5f;
if (xmb->under_item_offset < 3)
xmb->under_item_offset = 3.0f;
if ( !settings->bools.menu_show_sublabels
&& !settings->bools.menu_xmb_entry_icons)
{
xmb->active_item_factor = 3.0f / scale_cap;
xmb->above_subitem_offset = 3.0f / scale_cap;
xmb->under_item_offset = 3.0f / scale_cap;
}
xmb->font_size = new_font_size;
xmb->font2_size = 22.0f * scale_factor;
xmb->icon_spacing_horizontal = 192.0f * scale_factor / scale_cap;
xmb->icon_spacing_vertical = 64.0f * scale_factor;
xmb->margins_screen_top = (256 + 16) * scale_factor / scale_cap;
xmb->margins_screen_left = 336.0f * scale_factor / scale_cap;
xmb->margins_label_left = 85.0f * scale_factor / scale_cap;
xmb->margins_label_top = new_font_size / 3.0f;
xmb->margins_setting_left = 660.0f * scale_factor / scale_cap * xmb->scale_mod[6];
xmb_layout_common(xmb, scale_factor, new_font_size);
xmb->icon_size /= scale_cap;
}
static void xmb_layout_psp(xmb_handle_t *xmb, settings_t *settings)
{
float scale_factor = xmb->last_scale_factor;
float scale_cap = xmb->scale_cap;
unsigned new_font_size = 26 * scale_factor;
xmb->above_subitem_offset = 1.5f / scale_cap;
xmb->above_item_offset = -0.5f / scale_cap;
xmb->active_item_factor = 2.0f / scale_cap;
xmb->under_item_offset = 3.0f / scale_cap;
/* Scaling limits */
if (xmb->active_item_factor < 1)
xmb->active_item_factor = 1.0f;
if (xmb->above_subitem_offset < 1)
xmb->above_subitem_offset = 1.0f;
if (xmb->under_item_offset < 1)
xmb->under_item_offset = 1.0f;
if ( !settings->bools.menu_show_sublabels
&& !settings->bools.menu_xmb_entry_icons)
{
xmb->active_item_factor = 2.0f / scale_cap;
xmb->above_subitem_offset = 2.0f / scale_cap;
xmb->under_item_offset = 2.0f / scale_cap;
}
xmb->font_size = new_font_size;
xmb->font2_size = 18.0f * scale_factor;
xmb->icon_spacing_horizontal = 192.0f * scale_factor / scale_cap;
xmb->icon_spacing_vertical = 82.0f * scale_factor;
xmb->margins_screen_top = 192.0f * scale_factor / scale_cap;
xmb->margins_screen_left = 136.0f * scale_factor / scale_cap;
xmb->margins_label_left = 85.0f * scale_factor / scale_cap;
xmb->margins_label_top = new_font_size / 3.0f;
xmb->margins_setting_left = 520.0f * scale_factor / scale_cap * xmb->scale_mod[6];
xmb_layout_common(xmb, scale_factor, new_font_size);
xmb->icon_size /= scale_cap;
}
static void xmb_init_scale_mod(float *scale_mod, float scale_value)
{
uint8_t i;
/* scaling multiplier formulas made from these values: */
/* xmb_scale 50 = {2.5, 2.5, 2, 1.7, 2.5, 4, 2.4, 2.5} */
/* xmb_scale 75 = { 2, 1.6, 1.6, 1.4, 1.5, 2.3, 1.9, 1.3} */
{
/* text length & word wrap (base 35 apply to file browser, 1st column) */
scale_mod[0] = -0.03 * scale_value + 4.083;
/* playlist text length when thumbnail is ON (small, base 40) */
scale_mod[1] = -0.03 * scale_value + 3.95;
/* playlist text length when thumbnail is OFF (large, base 70) */
scale_mod[2] = -0.02 * scale_value + 3.033;
/* sub-label length & word wrap */
scale_mod[3] = -0.014 * scale_value + 2.416;
/* thumbnail size & vertical margin from top */
scale_mod[4] = -0.03 * scale_value + 3.916;
/* thumbnail horizontal left margin (horizontal positioning) */
scale_mod[5] = -0.06 * scale_value + 6.933;
/* margin before 2nd column start (shaders parameters, cheats...) */
scale_mod[6] = -0.024 * scale_value + 3.400;
/* text length & word wrap (base 35 apply to 2nd column in cheats, shaders, etc) */
scale_mod[7] = 134.179 * pow(scale_value, -1.0778);
for (i = 0; i < 8; i++)
if (scale_mod[i] < 1)
scale_mod[i] = 1;
}
}
/* Resettle the two horizontal offsets that are cached rather than
* recomputed per frame.
*
* Both are derived from values xmb_layout() has just changed —
* xmb->x from icon_size, xmb->categories_x_pos from
* icon_spacing_horizontal — but they are otherwise only written on a
* depth or category change. Left alone across a scale change they keep
* the offsets from the old scale, which slides the category icon and
* the entry column sideways until the user navigates out and back.
*
* Any tween in flight on either has a target computed at the old
* scale, so it has to be killed rather than allowed to finish; this
* mirrors what xmb_context_reset_horizontal_list() already does when
* it assigns these directly. */
static void xmb_refresh_horizontal_offsets(xmb_handle_t *xmb)
{
uintptr_t tag = (uintptr_t)&xmb->x;
uintptr_t cat_tag = (uintptr_t)&xmb->categories_x_pos;
int depth = (xmb->depth > 1) ? 2 : 1;
gfx_animation_kill_by_tag(&tag);
gfx_animation_kill_by_tag(&cat_tag);
xmb->categories_x_pos = xmb->icon_spacing_horizontal
* -(float)xmb->categories_selection_ptr;
/* Clamped the same way the settled value is elsewhere: the opening
* animation is only pushed for depth <= 2, so deeper stacks keep
* the depth-2 offset. */
xmb->x = xmb->icon_size
* (xmb->use_ps3_layout ? 1.1f : 0.7f) * -(depth * 2 - 2);
}
static void xmb_layout(xmb_handle_t *xmb)
{
unsigned i;
settings_t *settings = config_get_ptr();
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
size_t selection = menu_st->selection_ptr;
unsigned current = (unsigned)selection;
unsigned end = (unsigned)MENU_LIST_GET_SELECTION(menu_list, 0)->size;
xmb_init_scale_mod(xmb->scale_mod, settings->floats.menu_scale_factor * 100.0f);
xmb->scale_cap = (settings->floats.menu_scale_factor > 1.0f)
? settings->floats.menu_scale_factor
: 1.0f;
if (xmb->use_ps3_layout)
xmb_layout_ps3(xmb, settings);
else
xmb_layout_psp(xmb, settings);
for (i = 0; i < end; i++)
{
float ia = xmb->items_passive_alpha;
float iz = xmb->items_passive_zoom;
xmb_node_t *node = (xmb_node_t*)selection_buf->list[i].userdata;
if (!node)
continue;
if (i == current)
{
ia = xmb->items_active_alpha;
iz = xmb->items_active_alpha;
}
node->alpha = ia;
node->label_alpha = ia;
node->zoom = iz;
node->y = xmb_item_y(xmb, i, current);
}
/* node->y above is resettled from the new spacing; the horizontal
* offsets are cached rather than per-frame and need the same. */
xmb_refresh_horizontal_offsets(xmb);
}
static const char *xmb_texture_path(unsigned id)
{
switch (id)
{
case XMB_TEXTURE_MAIN_MENU:
#if defined(HAVE_LAKKA)
return "lakka.png";
#else
return "retroarch.png";
#endif
case XMB_TEXTURE_SETTINGS:
return "settings.png";
case XMB_TEXTURE_HISTORY:
return "history.png";
case XMB_TEXTURE_FAVORITES:
return "favorites.png";
case XMB_TEXTURE_ADD_FAVORITE:
return "add-favorite.png";
#ifdef HAVE_IMAGEVIEWER
case XMB_TEXTURE_IMAGES:
return "images.png";
#endif
case XMB_TEXTURE_MUSICS:
return "musics.png";
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
case XMB_TEXTURE_MOVIES:
return "movies.png";
#endif
case XMB_TEXTURE_SETTING:
return "setting.png";
case XMB_TEXTURE_SUBSETTING:
return "subsetting.png";
case XMB_TEXTURE_ARROW:
return "arrow.png";
case XMB_TEXTURE_RUN:
return "run.png";
case XMB_TEXTURE_CLOSE:
return "close.png";
case XMB_TEXTURE_RESUME:
return "resume.png";
case XMB_TEXTURE_CLOCK:
return "clock.png";
case XMB_TEXTURE_BATTERY_FULL:
return "battery-full.png";
case XMB_TEXTURE_BATTERY_CHARGING:
return "battery-charging.png";
case XMB_TEXTURE_BATTERY_80:
return "battery-80.png";
case XMB_TEXTURE_BATTERY_60:
return "battery-60.png";
case XMB_TEXTURE_BATTERY_40:
return "battery-40.png";
case XMB_TEXTURE_BATTERY_20:
return "battery-20.png";
case XMB_TEXTURE_POINTER:
return "pointer.png";
case XMB_TEXTURE_SAVESTATE:
return "savestate.png";
case XMB_TEXTURE_LOADSTATE:
return "loadstate.png";
case XMB_TEXTURE_RECORDREPLAY:
return "savestate.png";
case XMB_TEXTURE_PLAYREPLAY:
return "loadstate.png";
case XMB_TEXTURE_HALTREPLAY:
return "close.png";
case XMB_TEXTURE_UNDO:
return "undo.png";
case XMB_TEXTURE_CORE_INFO:
return "core-infos.png";
case XMB_TEXTURE_BLUETOOTH:
return "bluetooth.png";
case XMB_TEXTURE_WIFI:
return "wifi.png";
case XMB_TEXTURE_CORE_OPTIONS:
return "core-options.png";
case XMB_TEXTURE_INPUT_REMAPPING_OPTIONS:
return "core-input-remapping-options.png";
case XMB_TEXTURE_CHEAT_OPTIONS:
return "core-cheat-options.png";
case XMB_TEXTURE_DISK_OPTIONS:
return "core-disk-options.png";
case XMB_TEXTURE_SHADER_OPTIONS:
return "core-shader-options.png";
case XMB_TEXTURE_ACHIEVEMENT_LIST:
return "achievement-list.png";
case XMB_TEXTURE_SCREENSHOT:
return "screenshot.png";
case XMB_TEXTURE_RELOAD:
return "reload.png";
case XMB_TEXTURE_RENAME:
return "rename.png";
case XMB_TEXTURE_FILE:
return "file.png";
case XMB_TEXTURE_FOLDER:
return "folder.png";
case XMB_TEXTURE_ZIP:
return "zip.png";
case XMB_TEXTURE_FAVORITE:
return "favorites-content.png";
case XMB_TEXTURE_IMAGE:
return "image.png";
case XMB_TEXTURE_MUSIC:
return "music.png";
case XMB_TEXTURE_MOVIE:
return "movie.png";
case XMB_TEXTURE_CORE:
return "core.png";
case XMB_TEXTURE_RDB:
return "database.png";
case XMB_TEXTURE_CURSOR:
return "cursor.png";
case XMB_TEXTURE_SWITCH_ON:
return "on.png";
case XMB_TEXTURE_SWITCH_OFF:
return "off.png";
case XMB_TEXTURE_ADD:
return "add.png";
#ifdef HAVE_NETWORKING
case XMB_TEXTURE_NETPLAY:
return "netplay.png";
case XMB_TEXTURE_ROOM:
return "menu_room.png";
case XMB_TEXTURE_ROOM_LAN:
return "menu_room_lan.png";
case XMB_TEXTURE_ROOM_RELAY:
return "menu_room_relay.png";
#endif
case XMB_TEXTURE_KEY:
return "key.png";
case XMB_TEXTURE_KEY_HOVER:
return "key-hover.png";
case XMB_TEXTURE_DIALOG_SLICE:
return "dialog-slice.png";
case XMB_TEXTURE_ACHIEVEMENTS:
return "menu_achievements.png";
case XMB_TEXTURE_AUDIO:
return "menu_audio.png";
case XMB_TEXTURE_DRIVERS:
return "menu_drivers.png";
case XMB_TEXTURE_EXIT:
return "menu_exit.png";
case XMB_TEXTURE_FRAMESKIP:
return "menu_frameskip.png";
case XMB_TEXTURE_HELP:
return "menu_help.png";
case XMB_TEXTURE_INFO:
return "menu_info.png";
case XMB_TEXTURE_INPUT_SETTINGS:
return "Libretro - Pad.png";
case XMB_TEXTURE_LATENCY:
return "menu_latency.png";
case XMB_TEXTURE_NETWORK:
return "menu_network.png";
case XMB_TEXTURE_POWER:
return "menu_power.png";
case XMB_TEXTURE_RECORD:
return "menu_record.png";
case XMB_TEXTURE_SAVING:
return "menu_saving.png";
case XMB_TEXTURE_UPDATER:
return "menu_updater.png";
case XMB_TEXTURE_VIDEO:
return "menu_video.png";
case XMB_TEXTURE_MIXER:
return "menu_mixer.png";
case XMB_TEXTURE_LOG:
return "menu_log.png";
case XMB_TEXTURE_OSD:
return "menu_osd.png";
case XMB_TEXTURE_UI:
return "menu_ui.png";
case XMB_TEXTURE_USER:
return "menu_user.png";
case XMB_TEXTURE_PRIVACY:
return "menu_privacy.png";
case XMB_TEXTURE_PLAYLIST:
return "menu_playlist.png";
case XMB_TEXTURE_DISC:
return "disc.png";
case XMB_TEXTURE_QUICKMENU:
return "menu_quickmenu.png";
case XMB_TEXTURE_REWIND:
return "menu_rewind.png";
case XMB_TEXTURE_OVERLAY:
return "menu_overlay.png";
case XMB_TEXTURE_OVERRIDE:
return "menu_override.png";
case XMB_TEXTURE_NOTIFICATIONS:
return "menu_notifications.png";
case XMB_TEXTURE_STREAM:
return "menu_stream.png";
case XMB_TEXTURE_SHUTDOWN:
return "menu_shutdown.png";
case XMB_TEXTURE_INPUT_DPAD_U:
return "input_DPAD-U.png";
case XMB_TEXTURE_INPUT_DPAD_D:
return "input_DPAD-D.png";
case XMB_TEXTURE_INPUT_DPAD_L:
return "input_DPAD-L.png";
case XMB_TEXTURE_INPUT_DPAD_R:
return "input_DPAD-R.png";
case XMB_TEXTURE_INPUT_STCK_U:
return "input_STCK-U.png";
case XMB_TEXTURE_INPUT_STCK_D:
return "input_STCK-D.png";
case XMB_TEXTURE_INPUT_STCK_L:
return "input_STCK-L.png";
case XMB_TEXTURE_INPUT_STCK_R:
return "input_STCK-R.png";
case XMB_TEXTURE_INPUT_STCK_P:
return "input_STCK-P.png";
case XMB_TEXTURE_INPUT_BTN_U:
return "input_BTN-U.png";
case XMB_TEXTURE_INPUT_BTN_D:
return "input_BTN-D.png";
case XMB_TEXTURE_INPUT_BTN_L:
return "input_BTN-L.png";
case XMB_TEXTURE_INPUT_BTN_R:
return "input_BTN-R.png";
case XMB_TEXTURE_INPUT_LB:
return "input_LB.png";
case XMB_TEXTURE_INPUT_RB:
return "input_RB.png";
case XMB_TEXTURE_INPUT_LT:
return "input_LT.png";
case XMB_TEXTURE_INPUT_RT:
return "input_RT.png";
case XMB_TEXTURE_INPUT_SELECT:
return "input_SELECT.png";
case XMB_TEXTURE_INPUT_START:
return "input_START.png";
case XMB_TEXTURE_INPUT_ADC:
return "input_ADC.png";
case XMB_TEXTURE_INPUT_BIND_ALL:
return "input_BIND_ALL.png";
case XMB_TEXTURE_INPUT_MOUSE:
return "input_MOUSE.png";
case XMB_TEXTURE_INPUT_LGUN:
return "input_LGUN.png";
case XMB_TEXTURE_INPUT_TURBO:
return "input_TURBO.png";
case XMB_TEXTURE_CHECKMARK:
return "menu_check.png";
case XMB_TEXTURE_MENU_ADD:
return "menu_add.png";
case XMB_TEXTURE_BRIGHTNESS:
return "menu_brightness.png";
case XMB_TEXTURE_PAUSE:
return "menu_pause.png";
case XMB_TEXTURE_DEFAULT:
return "default.png";
case XMB_TEXTURE_DEFAULT_CONTENT:
return "default-content.png";
case XMB_TEXTURE_MENU_APPLY_TOGGLE:
return "menu_apply_toggle.png";
case XMB_TEXTURE_MENU_APPLY_COG:
return "menu_apply_cog.png";
}
return NULL;
}
/* xmb keeps no font_data_impl_t, so it does not get the refresh
* font_flush() gives ozone and materialui. Only wideglyph_width is
* derived from the face here, so that is all there is to redo. */
static void xmb_compute_wideglyph(xmb_handle_t *xmb)
{
if (!xmb || !xmb->font)
return;
xmb->wideglyph_generation = font_driver_get_generation();
xmb->wideglyph_width = 100;
if (xmb->wideglyph_str)
{
int char_width = font_driver_get_message_width(
xmb->font, "a", 1, 1.0f);
int wideglyph_width = font_driver_get_message_width(
xmb->font, xmb->wideglyph_str,
strlen(xmb->wideglyph_str), 1.0f);
if (wideglyph_width > 0 && char_width > 0)
xmb->wideglyph_width = wideglyph_width * 100 / char_width;
}
}
/* Unconditional above, guarded here. A fresh xmb_handle_t and a fresh
* font_driver both start at generation 0, so a caller that knows the
* font is new has to bypass the check or the width is never worked
* out at all. */
static void xmb_sync_wideglyph(xmb_handle_t *xmb)
{
if (!xmb || !xmb->font)
return;
if (xmb->wideglyph_generation == font_driver_get_generation())
return;
xmb_compute_wideglyph(xmb);
}
static void xmb_context_reset_textures(
xmb_handle_t *xmb,
const char *iconpath,
unsigned menu_xmb_theme)
{
unsigned i;
/* Invalidate in-flight context texture loads */
xmb_ctx_icon_load_gen++;
for (i = 0; i < XMB_TEXTURE_LAST; i++)
{
char texpath[PATH_MAX_LENGTH];
const char *texture_path = xmb_texture_path(i);
fill_pathname_join_special(texpath,
iconpath, texture_path, sizeof(texpath));
gfx_display_reset_icon_texture(texpath,
&xmb->textures.list[i], gfx_display_texture_filter(),
NULL, NULL);
}
xmb->main_menu_node.icon = 0;
xmb->settings_tab_node.icon = 0;
xmb->history_tab_node.icon = 0;
xmb->favorites_tab_node.icon = 0;
#ifdef HAVE_IMAGEVIEWER
xmb->images_tab_node.icon = 0;
#endif
xmb->music_tab_node.icon = 0;
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
xmb->video_tab_node.icon = 0;
#endif
xmb->add_tab_node.icon = 0;
xmb->contentless_cores_tab_node.icon = 0;
#if defined(HAVE_LIBRETRODB)
xmb->explore_tab_node.icon = 0;
#endif
#ifdef HAVE_NETWORKING
xmb->netplay_tab_node.icon = 0;
#endif
xmb->main_menu_node.alpha = xmb->categories_active_alpha;
xmb->main_menu_node.zoom = xmb->categories_active_zoom;
xmb->settings_tab_node.alpha = xmb->categories_active_alpha;
xmb->settings_tab_node.zoom = xmb->categories_active_zoom;
xmb->history_tab_node.alpha = xmb->categories_active_alpha;
xmb->history_tab_node.zoom = xmb->categories_active_zoom;
xmb->favorites_tab_node.alpha = xmb->categories_active_alpha;
xmb->favorites_tab_node.zoom = xmb->categories_active_zoom;
#ifdef HAVE_IMAGEVIEWER
xmb->images_tab_node.alpha = xmb->categories_active_alpha;
xmb->images_tab_node.zoom = xmb->categories_active_zoom;
#endif
xmb->music_tab_node.alpha = xmb->categories_active_alpha;
xmb->music_tab_node.zoom = xmb->categories_active_zoom;
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
xmb->video_tab_node.alpha = xmb->categories_active_alpha;
xmb->video_tab_node.zoom = xmb->categories_active_zoom;
#endif
xmb->add_tab_node.alpha = xmb->categories_active_alpha;
xmb->add_tab_node.zoom = xmb->categories_active_zoom;
xmb->contentless_cores_tab_node.alpha = xmb->categories_active_alpha;
xmb->contentless_cores_tab_node.zoom = xmb->categories_active_zoom;
#if defined(HAVE_LIBRETRODB)
xmb->explore_tab_node.alpha = xmb->categories_active_alpha;
xmb->explore_tab_node.zoom = xmb->categories_active_zoom;
#endif
#ifdef HAVE_NETWORKING
xmb->netplay_tab_node.alpha = xmb->categories_active_alpha;
xmb->netplay_tab_node.zoom = xmb->categories_active_zoom;
#endif
xmb->current_menu_icon = 0;
/* Recolor */
if ( menu_xmb_theme == XMB_ICON_THEME_MONOCHROME_INVERTED
|| menu_xmb_theme == XMB_ICON_THEME_AUTOMATIC_INVERTED)
memcpy(xmb->item_color_base, xmb_color_black, sizeof(xmb->item_color_base));
else
memcpy(xmb->item_color_base, xmb_color_white, sizeof(xmb->item_color_base));
}
/* Build xmb->font / xmb->font2 at the sizes xmb_layout() just set.
*
* With defer_free set the previous handles are retired rather than
* freed, so this is safe to call from xmb_render(); the caller gets
* correctly sized fonts on the very next draw either way. Path
* resolution lives here rather than at the call sites so that a
* context reset and an in-place rebuild cannot disagree on what is
* loaded. */
static bool xmb_rebuild_fonts(xmb_handle_t *xmb, bool is_threaded,
bool defer_free)
{
char fontpath[PATH_MAX_LENGTH];
char default_fontpath[PATH_MAX_LENGTH];
char pkg_dir[DIR_MAX_LENGTH];
gfx_display_t *p_disp = disp_get_ptr();
settings_t *settings = config_get_ptr();
const char *lang_font = font_driver_language_font_file();
const char *menu_font = settings->paths.path_menu_xmb_font;
font_data_t *old_font = xmb->font;
font_data_t *old_font2 = xmb->font2;
fontpath[0] = '\0';
/* The theme's font, then the language override on top of it. Both
* are kept: the first is what to fall back to when the language
* wants nothing special, and telling the font driver both is what
* lets a language change rebuild these in place. */
fill_pathname_application_special(
default_fontpath, sizeof(default_fontpath),
APPLICATION_SPECIAL_DIRECTORY_ASSETS_XMB_FONT);
fill_pathname_join_special(pkg_dir,
settings->paths.directory_assets, "pkg", sizeof(pkg_dir));
/* An explicit menu font wins, and there is nothing to
* re-resolve in that case. */
if (lang_font && !(menu_font && *menu_font))
fill_pathname_join_special(fontpath, pkg_dir, lang_font,
sizeof(fontpath));
else
strlcpy(fontpath, default_fontpath, sizeof(fontpath));
/* Recorded before the match test, not after it: the setting can
* change to a path that resolves to the file already loaded, and
* leaving it stale would have xmb_render() detect a change every
* frame and call back in here forever. */
strlcpy(xmb->last_font_path, menu_font ? menu_font : "",
sizeof(xmb->last_font_path));
/* Both already the fonts that were asked for. Most steps of the
* scale slider land here, since the setting moves in hundredths
* and font_size is an integer, and a title-margin change never
* moves either size. */
if ( font_driver_matches(xmb->font, fontpath, xmb->font_size)
&& font_driver_matches(xmb->font2, fontpath, xmb->font2_size))
return false;
/* Build before releasing, so the old atlas stays valid for any
* frame still in flight and there is no window with no font at
* all. */
xmb->font = gfx_display_font_file(p_disp,
fontpath, xmb->font_size, is_threaded);
xmb->font2 = gfx_display_font_file(p_disp,
fontpath, xmb->font2_size, is_threaded);
if (!(menu_font && *menu_font))
{
font_driver_set_language_font(xmb->font, pkg_dir, default_fontpath);
font_driver_set_language_font(xmb->font2, pkg_dir, default_fontpath);
}
if (defer_free)
{
font_driver_free_deferred(old_font);
font_driver_free_deferred(old_font2);
}
else
{
if (old_font)
font_driver_free(old_font);
if (old_font2)
font_driver_free(old_font2);
}
return true;
}
/* Apply a scale factor / layout change in place.
*
* This is the XMB counterpart to ozone_set_layout(): a scale change
* needs the geometry recomputed and the fonts rebuilt at the new
* sizes, and nothing else. The thumbnail, wallpaper, horizontal-list
* and screensaver work in xmb_context_reset_internal() exists for an
* actual graphics context reset and is scale-independent, so it is not
* run here — which also keeps the texture-freeing calls out of
* xmb_render(), where they are exactly what is unsafe. */
static void xmb_set_layout(xmb_handle_t *xmb, bool is_threaded)
{
const char *wideglyph_str = msg_hash_get_wideglyph_str();
bool wideglyph_changed = (xmb->wideglyph_str != wideglyph_str);
bool rebuilt;
/* The geometry is cheap and always applied. Rasterising the two
* atlases is not, so xmb_rebuild_fonts() decides for itself
* whether anything it would build differs from what is loaded. */
xmb_layout(xmb);
rebuilt = xmb_rebuild_fonts(xmb, is_threaded, true);
/* The wide-glyph sample follows the menu language and can move
* without the face doing so, in which case the widths derived from
* it still need recomputing against the font already loaded. */
if (rebuilt || wideglyph_changed)
{
xmb->wideglyph_str = wideglyph_str;
xmb_compute_wideglyph(xmb);
}
/* Title metrics are measured against the font. */
if (rebuilt)
xmb_set_title(xmb);
}
static void xmb_context_reset_internal(xmb_handle_t *xmb,
bool is_threaded, bool reinit_textures, unsigned menu_xmb_theme)
{
char iconpath[PATH_MAX_LENGTH];
struct menu_state *menu_st = menu_state_get_ptr();
const char *wideglyph_str = msg_hash_get_wideglyph_str();
iconpath[0] = '\0';
fill_pathname_application_special(iconpath, sizeof(iconpath),
APPLICATION_SPECIAL_DIRECTORY_ASSETS_XMB_ICONS);
xmb_layout(xmb);
xmb_rebuild_fonts(xmb, is_threaded, false);
xmb->wideglyph_str = wideglyph_str;
xmb_compute_wideglyph(xmb);
if (reinit_textures)
{
gfx_display_deinit_white_texture();
gfx_display_init_white_texture();
xmb->assets_missing = false;
xmb_context_reset_textures(xmb, iconpath, menu_xmb_theme);
if (!path_is_valid(iconpath))
xmb->assets_missing = true;
}
xmb->allow_horizontal_animation = true;
xmb->allow_dynamic_wallpaper = true;
xmb_context_reset_horizontal_list(xmb);
xmb_set_title(xmb);
/* Must run after xmb_set_title(): xmb_path_dynamic_wallpaper()
* reads xmb->title_name to build the wallpaper filename. The
* previous order invoked this with a stale title_name from the
* prior stack position (or empty-string on cold boot, since
* xmb_handle_t is calloc-initialized), so the wallpaper loaded
* here could briefly be the wrong one until the next
* xmb_populate_entries -> xmb_update_dynamic_wallpaper chain
* corrected it. xmb_populate_entries already uses the correct
* set_title -> update_dynamic_wallpaper order. */
xmb_update_dynamic_wallpaper(xmb, true);
menu_screensaver_context_destroy(xmb->screensaver);
/* Only reload thumbnails if:
* > Thumbnails are enabled
* > This is a playlist, a database list, a file list
* or the quick menu */
if ( gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
|| gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON))
{
if ( xmb->is_playlist
|| xmb->is_db_manager_list
|| xmb->is_explore_list
|| xmb->is_file_list
|| xmb->is_quick_menu)
xmb_update_thumbnail_image(xmb);
if (xmb->is_playlist)
{
/* Synchronous populate — clear any pending deferred flag so
* xmb_render does not redundantly repeat this work once input
* settles. */
xmb->pending_dynamic_icons_repopulate = false;
xmb_populate_dynamic_icons(xmb);
}
}
/* Have to reset this, otherwise savestate
* thumbnail won't update after fullscreen toggle */
gfx_thumbnail_reset(&xmb->thumbnails.savestate);
xmb_update_savestate_thumbnail_image(xmb);
}
static void xmb_render(void *data,
unsigned width, unsigned height, bool is_idle)
{
size_t i;
/* c.f. https://gcc.gnu.org/bugzilla/show_bug.cgi?id=323
* On some platforms (e.g. 32-bit x86 without SSE),
* gcc can produce inconsistent floating point results
* depending upon optimisation level. This can break
* floating point variable comparisons. A workaround is
* to declare the affected variable as 'volatile', which
* disables optimisations and removes excess precision
* (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=323#c87) */
volatile float scale_factor;
bool use_ps3_layout;
xmb_handle_t *xmb = (xmb_handle_t*)data;
settings_t *settings = config_get_ptr();
struct menu_state *menu_st = menu_state_get_ptr();
menu_input_t *menu_input = &menu_st->input_state;
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
size_t end = selection_buf->size;
gfx_display_t *p_disp = disp_get_ptr();
gfx_animation_t *p_anim = anim_get_ptr();
size_t selection = menu_st->selection_ptr;
playlist_t *playlist = playlist_get_cached();
unsigned first = 0;
unsigned last = (unsigned)end;
unsigned gfx_thumbnail_upscale_threshold = settings->uints.gfx_thumbnail_upscale_threshold;
bool network_on_demand_thumbnails = settings->bools.network_on_demand_thumbnails;
if (!xmb)
return;
/* Advance animated thumbnails (animated WebP) once per frame on the
* main thread. No-op for still images. */
gfx_thumbnail_animate(&xmb->thumbnails.right);
gfx_thumbnail_animate(&xmb->thumbnails.left);
gfx_thumbnail_animate(&xmb->thumbnails.icon);
/* Fire deferred dynamic-icon repopulate once input has settled.
* Set by xmb_populate_entries when it wanted to run the work but
* the user was mid-mash (menu_st->scroll.acceleration > 0) — the
* intermediate tabs blown through during held-repeat don't get
* the populate, only whichever tab the user actually stops on.
* Re-checks is_playlist and menu_icon_thumbnails at fire time:
* the list that was pushed when the flag was set may no longer
* be the list being looked at now, and the gate uses the
* *current* state. If the final list is not a playlist, the
* pending-icons unload branch from populate_entries runs here
* instead. */
if ( xmb->pending_dynamic_icons_repopulate
&& (menu_st->scroll.acceleration == 0))
{
xmb->pending_dynamic_icons_repopulate = false;
if (xmb->is_playlist)
{
if (settings->uints.menu_icon_thumbnails)
xmb_populate_dynamic_icons(xmb);
}
else if (xmb->thumbnails.pending_icons != XMB_PENDING_THUMBNAIL_NONE)
xmb_unload_icon_thumbnail_textures(xmb);
}
/* Retry current-menu-icon resolution if a previous xmb_set_title()
* (typically the one invoked from xmb_context_reset_internal() after
* a fullscreen toggle) could not resolve the icon because an
* async-loaded sidebar or db-node icon handle was still 0.
* xmb_set_title() decrements the counter on each retry and clears
* it on success, so the loop terminates either way. */
if (xmb->current_menu_icon_retry > 0)
xmb_set_title(xmb);
use_ps3_layout = xmb_use_ps3_layout(settings->uints.menu_xmb_layout, width, height);
scale_factor = xmb_get_scale_factor(settings->floats.menu_scale_factor,
use_ps3_layout, width);
if ( (use_ps3_layout != xmb->last_use_ps3_layout)
|| (xmb->margins_title != xmb->last_margins_title)
|| (xmb->margins_title_horizontal_offset != xmb->last_margins_title_horizontal_offset)
|| (scale_factor != xmb->last_scale_factor)
|| !string_is_equal(xmb->last_font_path,
settings->paths.path_menu_xmb_font))
{
/* Applied here and now, in full. The next frame drawn is the
* first one at the new scale, so moving the setting moves the
* menu: geometry and fonts change together and there is no
* window in which they disagree.
*
* Deferring this was what made the two disagree, and the frame
* skip that hid the disagreement was the flash. The GPU hazard
* that motivated the deferral is narrower than the rebuild:
* only releasing the superseded font handles has to wait, and
* xmb_rebuild_fonts() handles that by building the replacements
* first and retiring the old ones. */
xmb->use_ps3_layout = use_ps3_layout;
xmb->last_use_ps3_layout = use_ps3_layout;
xmb->last_scale_factor = scale_factor;
xmb->last_margins_title = xmb->margins_title;
xmb->last_margins_title_horizontal_offset = xmb->margins_title_horizontal_offset;
xmb_set_layout(xmb, video_driver_is_threaded());
}
/* This must be set every frame when using a pointer,
* otherwise touchscreen input breaks when changing
* orientation */
p_disp->framebuf_width = width;
p_disp->framebuf_height = height;
/* Read pointer state */
menu_input_get_pointer_state(&xmb->pointer);
/* Direction locking for horizontal category dragging */
if (xmb->pointer.type != MENU_POINTER_DISABLED)
{
if ( xmb->pointer.flags & MENU_INP_PTR_FLG_PRESSED
&& !*menu_st->driver_data->menu_state_msg)
{
if (xmb->drag_mode == XMB_DRAG_DETECTING)
{
/* Wait for movement threshold before locking direction */
float dx = fabs((float)xmb->pointer.x - xmb->drag_start_x);
float dy = fabs((float)xmb->pointer.y - xmb->drag_start_y);
float threshold = 10.0f;
if (dx > threshold || dy > threshold)
{
/* Lock to dominant direction */
if (dx > dy)
xmb->drag_mode = XMB_DRAG_HORIZONTAL;
else
xmb->drag_mode = XMB_DRAG_VERTICAL;
}
}
if (xmb->drag_mode == XMB_DRAG_HORIZONTAL)
{
/* Only allow horizontal category switching at the top level (depth == 1)
* When in submenus (depth > 1), horizontal drag is disabled */
if (xmb->depth == 1 && xmb->show_horizontal_list)
{
/* Apply horizontal drag to categories */
size_t list_size = xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL) + xmb->system_tab_end + 1;
float current_dx = (float)xmb->pointer.x - xmb->drag_start_x;
float min_x = -xmb->icon_spacing_horizontal * (float)(list_size > 0 ? list_size - 1 : 0);
float max_x = 0.0f;
xmb->categories_x_pos = xmb->categories_drag_start_pos + current_dx;
/* Bounds checking */
if (xmb->categories_x_pos > max_x)
xmb->categories_x_pos = max_x;
if (xmb->categories_x_pos < min_x)
xmb->categories_x_pos = min_x;
/* Update selection and switch categories during drag */
{
float normalized = -xmb->categories_x_pos / xmb->icon_spacing_horizontal;
size_t nearest = (size_t)(normalized + 0.5f);
/* Clamp to valid range */
if (nearest >= list_size)
nearest = list_size > 0 ? list_size - 1 : 0;
/* If category changed, do full switch */
if (nearest != xmb->categories_selection_ptr)
{
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
/* Determine direction */
enum menu_action action = (nearest > xmb->categories_selection_ptr)
? MENU_ACTION_RIGHT
: MENU_ACTION_LEFT;
/* Call list_cache to update state (increments/decrements by 1) */
if (menu_st->driver_ctx && menu_st->driver_ctx->list_cache)
menu_st->driver_ctx->list_cache(menu_st->userdata, MENU_LIST_HORIZONTAL, action);
/* Repopulate vertical list with new category content */
menu_driver_deferred_push_content_list(selection_buf);
/* Visual update without animation during drag */
xmb_list_switch_horizontal_list(xmb, false, 0);
}
}
}
}
else if (xmb->drag_mode == XMB_DRAG_VERTICAL)
{
/* Apply vertical drag to list selection */
menu_list_t *menu_list = menu_st->entries.list;
size_t list_size = MENU_LIST_GET_SELECTION(menu_list, 0)->size;
/* Calculate how many items to move based on drag distance */
float dy = (float)xmb->pointer.y - xmb->drag_start_y;
float item_height = xmb->icon_spacing_vertical;
/* Convert drag distance to item steps with threshold */
int steps = (int)((dy + (dy > 0 ? item_height * 0.5f : -item_height * 0.5f)) / item_height);
/* Calculate new selection (negative because dragging down = scroll up) */
int new_selection = (int)xmb->drag_start_selection - steps;
/* Clamp to valid range */
if (new_selection < 0)
new_selection = 0;
if (new_selection >= (int)list_size)
new_selection = list_size > 0 ? (int)list_size - 1 : 0;
/* Update selection if changed */
if ((size_t)new_selection != menu_st->selection_ptr)
{
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
uintptr_t tag = (uintptr_t)selection_buf;
menu_st->selection_ptr = (size_t)new_selection;
/* Kill existing animations to prevent stacking during continuous drag */
gfx_animation_kill_by_tag(&tag);
/* Enable animations for smooth scrolling */
xmb_navigation_set(xmb, true);
}
}
}
}
/* If menu screensaver is active, update
* screensaver and return */
if (xmb->show_screensaver)
{
menu_screensaver_iterate(
xmb->screensaver,
p_disp, p_anim,
(enum menu_screensaver_effect)settings->uints.menu_screensaver_animation,
settings->floats.menu_screensaver_animation_speed,
XMB_SCREENSAVER_TINT,
width, height,
settings->paths.directory_assets);
GFX_ANIMATION_CLEAR_ACTIVE(p_anim);
return;
}
if (xmb->pointer.type != MENU_POINTER_DISABLED)
{
size_t selection = menu_st->selection_ptr;
int16_t margin_top = (int16_t)xmb->margins_screen_top;
int16_t margin_left = (int16_t)xmb->margins_screen_left;
int16_t margin_right = (int16_t)((float)width - xmb->margins_screen_left);
int16_t pointer_x = xmb->pointer.x;
int16_t pointer_y = xmb->pointer.y;
/* When determining current pointer selection, we
* only track pointer movements between the left
* and right screen margins */
if ( (pointer_x > margin_left)
&& (pointer_x < margin_right))
{
unsigned first = 0;
unsigned last = (unsigned)end;
if (height)
xmb_calculate_visible_range(xmb, height,
end, (unsigned)selection, &first, &last);
for (i = (size_t)first; i <= (size_t)last; i++)
{
float entry_size = (i == (unsigned)selection) ?
xmb->icon_spacing_vertical * xmb->active_item_factor : xmb->icon_spacing_vertical;
float half_entry_size = entry_size * 0.5f;
float y_curr = xmb_item_y(xmb, (int)i, selection) + xmb->margins_screen_top;
int y1 = (int)((y_curr - half_entry_size) + 0.5f);
int y2 = (int)((y_curr + half_entry_size) + 0.5f);
if ( (pointer_y > y1)
&& (pointer_y < y2))
{
menu_input->ptr = (unsigned)i;
break;
}
}
}
/* Areas beyond the top/right margins are used
* as a sort of virtual dpad:
* - Above top margin: navigate left/right
* - Beyond right margin: navigate up/down
* Skip during drag operations to avoid fighting with drag scrolling */
if ( (xmb->drag_mode == XMB_DRAG_NONE)
&& ((pointer_y < margin_top)
|| (pointer_x > margin_right)))
{
menu_entry_t entry;
bool get_entry = false;
switch (xmb->pointer.press_direction)
{
case MENU_INPUT_PRESS_DIRECTION_UP:
if (pointer_x > margin_right)
get_entry = true;
break;
case MENU_INPUT_PRESS_DIRECTION_DOWN:
/* Note: Direction is inverted, since 'down' should
* move list downwards */
if (pointer_x > margin_right)
get_entry = true;
break;
case MENU_INPUT_PRESS_DIRECTION_LEFT:
/* Navigate left
* Note: At the top level, navigating left
* means switching to the 'next' horizontal list,
* which is actually a movement to the *right* */
if (pointer_y < margin_top)
get_entry = true;
break;
case MENU_INPUT_PRESS_DIRECTION_RIGHT:
/* Navigate right
* Note: At the top level, navigating right
* means switching to the 'previous' horizontal list,
* which is actually a movement to the *left* */
if (pointer_y < margin_top)
get_entry = true;
break;
case MENU_INPUT_PRESS_DIRECTION_NONE:
default:
break;
}
if (get_entry)
{
MENU_ENTRY_INITIALIZE(entry);
menu_entry_get(&entry, 0, selection, NULL, true);
}
switch (xmb->pointer.press_direction)
{
case MENU_INPUT_PRESS_DIRECTION_UP:
/* Note: Direction is inverted, since 'up' should
* move list upwards */
if (pointer_x > margin_right)
xmb_menu_entry_action(xmb, &entry, selection, MENU_ACTION_DOWN);
break;
case MENU_INPUT_PRESS_DIRECTION_DOWN:
/* Note: Direction is inverted, since 'down' should
* move list downwards */
if (pointer_x > margin_right)
xmb_menu_entry_action(xmb, &entry, selection, MENU_ACTION_UP);
break;
case MENU_INPUT_PRESS_DIRECTION_LEFT:
/* Navigate left
* Note: At the top level, navigating left
* means switching to the 'next' horizontal list,
* which is actually a movement to the *right* */
if (pointer_y < margin_top)
xmb_menu_entry_action(xmb,
&entry, selection,
(xmb->depth == 1)
? MENU_ACTION_RIGHT
: MENU_ACTION_LEFT);
break;
case MENU_INPUT_PRESS_DIRECTION_RIGHT:
/* Navigate right
* Note: At the top level, navigating right
* means switching to the 'previous' horizontal list,
* which is actually a movement to the *left* */
if (pointer_y < margin_top)
xmb_menu_entry_action(xmb,
&entry, selection,
(xmb->depth == 1)
? MENU_ACTION_LEFT
: MENU_ACTION_RIGHT);
break;
default:
/* Do nothing */
break;
}
}
}
/* Handle any pending icon thumbnail load requests */
if (xmb->thumbnails.pending_icons != XMB_PENDING_THUMBNAIL_NONE)
{
/* Walk the visible range and dispatch async stream requests for
* each unresolved entry. We deliberately do NOT sync-load on
* the main thread here. Doing so blocks the main thread long
* enough (PNG/JPEG decode + GPU texture upload, repeated for
* 10–20 entries) that the runloop misses several
* gfx_animation_update ticks — the next tick advances
* delta_time by the full elapsed wall-clock and tween easing
* functions evaluate near or past their duration, snapping
* any in-flight animation to its end state. The visible
* effect: the enter-playlist or tab-switch animation jumps
* straight to the end on the load frame.
*
* Async decode + upload runs on a worker thread and lands via
* task callback over the next several frames, keeping the
* main thread responsive and animations smooth. Path
* resolution stays inline (it's a few stat syscalls per
* unresolved entry, cheap enough not to block a frame). */
if (height)
xmb_calculate_visible_range(xmb, height, end, (unsigned)selection, &first, &last);
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_NONE;
for (i = first; i <= last; i++)
{
xmb_node_t *node = (xmb_node_t*)selection_buf->list[i].userdata;
xmb_icons_t *thumbnail_icon;
if (!node)
continue;
/* Allocates the ~15 KB icon block on first use. Only entries
* that reach this loop -- the visible window of a playlist,
* with icon thumbnails enabled -- ever pay for one. */
if (!(thumbnail_icon = xmb_node_icons_get(node)))
continue;
/* Already resolved and dispatched — nothing to do. */
if (thumbnail_icon->icon.status != GFX_THUMBNAIL_STATUS_UNKNOWN)
continue;
/* Lazy path resolution: if icon_path is empty (fresh populate
* or prior unload cleared it), resolve now. This is the work
* xmb_populate_dynamic_icons and xmb_selection_pointer_changed
* used to pay eagerly for every visible entry on every change,
* including on each vertical scroll step — a stat-syscall
* storm multiplied up to ×7 by playlist_allow_non_png's extension
* fallback chain. Doing it here, once per entry per list
* population, eliminates the repeat cost on scroll. */
if (!*thumbnail_icon->thumbnail_path_data.icon_path)
xmb_set_dynamic_icon_content(xmb, NULL, i, thumbnail_icon);
/* Dispatch async. gfx_thumbnail_request_stream tolerates an
* empty icon_path: it transitions status to MISSING on the
* next frame so we stop retrying. */
gfx_thumbnail_request_stream(
&thumbnail_icon->thumbnail_path_data,
p_anim,
GFX_THUMBNAIL_ICON,
playlist, i,
&thumbnail_icon->icon,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
/* Status will be UNKNOWN until stream_delay elapses, then
* transitions PENDING (task queued) or MISSING (no path).
* Either way it leaves UNKNOWN within a few frames. While
* UNKNOWN, raise pending_icons so we keep ticking the
* stream's delay_timer via gfx_thumbnail_request_stream
* on subsequent frames; the dispatch above is idempotent
* (it returns early once status is no longer UNKNOWN). */
if (thumbnail_icon->icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending_icons = XMB_PENDING_THUMBNAIL_ICONS;
}
}
else if (xmb->thumbnails.icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN
&& xmb->thumbnails.pending == XMB_PENDING_THUMBNAIL_NONE)
{
xmb_node_t *node = (xmb_node_t*)selection_buf->list[selection].userdata;
if (node)
{
xmb_icons_t *thumbnail_icon = xmb_node_icons_get(node);
if (thumbnail_icon)
{
gfx_thumbnail_request_stream(
&thumbnail_icon->thumbnail_path_data,
p_anim,
GFX_THUMBNAIL_ICON,
playlist, selection,
&xmb->thumbnails.icon,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
if (xmb->thumbnails.icon.status == GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_ICONS;
}
}
}
/* Handle any pending thumbnail load requests */
if (xmb->thumbnails.pending != XMB_PENDING_THUMBNAIL_NONE)
{
size_t selection = menu_st->selection_ptr;
playlist_t *playlist = playlist_get_cached();
unsigned gfx_thumbnail_upscale_threshold = settings->uints.gfx_thumbnail_upscale_threshold;
bool network_on_demand_thumbnails = settings->bools.network_on_demand_thumbnails;
/* Explore list needs cached selection index */
if (xmb->is_explore_list)
selection = menu_st->thumbnail_path_data ? menu_st->thumbnail_path_data->playlist_index : 0;
switch (xmb->thumbnails.pending)
{
case XMB_PENDING_THUMBNAIL_BOTH:
gfx_thumbnail_request_streams(
menu_st->thumbnail_path_data,
p_anim,
playlist, selection,
&xmb->thumbnails.right,
&xmb->thumbnails.left,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
if ( xmb->thumbnails.right.status != GFX_THUMBNAIL_STATUS_UNKNOWN
&& xmb->thumbnails.left.status != GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
break;
case XMB_PENDING_THUMBNAIL_RIGHT:
gfx_thumbnail_request_stream(
menu_st->thumbnail_path_data,
p_anim,
GFX_THUMBNAIL_RIGHT,
playlist, selection,
&xmb->thumbnails.right,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
if (xmb->thumbnails.right.status != GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
break;
case XMB_PENDING_THUMBNAIL_LEFT:
gfx_thumbnail_request_stream(
menu_st->thumbnail_path_data,
p_anim,
GFX_THUMBNAIL_LEFT,
playlist, selection,
&xmb->thumbnails.left,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
if (xmb->thumbnails.left.status != GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
break;
case XMB_PENDING_THUMBNAIL_ICONS:
gfx_thumbnail_request_stream(
menu_st->thumbnail_path_data,
p_anim,
GFX_THUMBNAIL_ICON,
playlist, selection,
&xmb->thumbnails.icon,
gfx_thumbnail_upscale_threshold,
network_on_demand_thumbnails);
if (xmb->thumbnails.icon.status != GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
break;
default:
break;
}
if (xmb->thumbnails.left.status != GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails_left_status_prev = xmb->thumbnails.left.status;
if (xmb->thumbnails.right.status != GFX_THUMBNAIL_STATUS_UNKNOWN)
xmb->thumbnails_right_status_prev = xmb->thumbnails.right.status;
}
i = menu_st->entries.begin;
if (i >= end)
menu_st->entries.begin = 0;
GFX_ANIMATION_CLEAR_ACTIVE(p_anim);
}
XMB_NOINLINE static void xmb_draw_bg(
void *userdata,
gfx_display_t *p_disp,
gfx_display_ctx_driver_t *dispctx,
unsigned video_width,
unsigned video_height,
unsigned menu_shader_pipeline,
unsigned xmb_color_theme,
float menu_wallpaper_opacity,
bool libretro_running,
float alpha,
uintptr_t texture_id,
const char *bg_file_path,
float *coord_black,
float *coord_white)
{
gfx_display_ctx_draw_t draw;
struct video_coords coords;
draw.x = 0;
draw.y = 0;
draw.texture = texture_id;
draw.width = video_width;
draw.height = video_height;
draw.color = &coord_black[0];
draw.vertex = NULL;
draw.tex_coord = NULL;
draw.vertex_count = 4;
draw.pipeline_id = 0;
draw.pipeline_active = (menu_shader_pipeline == XMB_SHADER_PIPELINE_WALLPAPER) ? false : true;
if (!video_width || !video_height)
return;
if (dispctx->blend_begin)
dispctx->blend_begin(userdata);
/* Draw background wallpaper */
if ( draw.texture
&& ( xmb_color_theme == XMB_THEME_WALLPAPER
|| ((bg_file_path && *bg_file_path) && !strstr(bg_file_path, FILE_PATH_BACKGROUND_IMAGE))))
{
draw.color = &coord_white[0];
gfx_display_set_alpha(draw.color, coord_white[3]);
gfx_display_draw_bg(p_disp, &draw, &coords, userdata, true, menu_wallpaper_opacity);
if (dispctx->draw)
dispctx->draw(&draw, userdata, video_width, video_height);
}
/* Draw empty color theme gradient */
else
{
draw.color = xmb_gradient_ident(xmb_color_theme);
draw.texture = 0;
gfx_display_set_alpha(draw.color, coord_white[3]);
gfx_display_draw_bg(p_disp, &draw, &coords, userdata, true, alpha);
if (dispctx->draw)
dispctx->draw(&draw, userdata, video_width, video_height);
}
#ifdef HAVE_SHADERPIPELINE
/* Draw pipeline */
if (menu_shader_pipeline > XMB_SHADER_PIPELINE_WALLPAPER)
{
switch (menu_shader_pipeline)
{
default:
case XMB_SHADER_PIPELINE_WALLPAPER:
draw.pipeline_id = VIDEO_SHADER_STOCK_BLEND;
break;
case XMB_SHADER_PIPELINE_RIBBON:
draw.pipeline_id = VIDEO_SHADER_MENU;
break;
case XMB_SHADER_PIPELINE_SIMPLE_RIBBON:
draw.pipeline_id = VIDEO_SHADER_MENU_2;
break;
#if !defined(VITA)
case XMB_SHADER_PIPELINE_SIMPLE_SNOW:
draw.pipeline_id = VIDEO_SHADER_MENU_3;
break;
case XMB_SHADER_PIPELINE_SNOW:
draw.pipeline_id = VIDEO_SHADER_MENU_4;
break;
case XMB_SHADER_PIPELINE_BOKEH:
draw.pipeline_id = VIDEO_SHADER_MENU_5;
break;
case XMB_SHADER_PIPELINE_SNOWFLAKE:
draw.pipeline_id = VIDEO_SHADER_MENU_6;
break;
#endif
}
if (dispctx->draw_pipeline)
dispctx->draw_pipeline(&draw, p_disp,
userdata, video_width, video_height);
if (dispctx->draw)
dispctx->draw(&draw, userdata, video_width, video_height);
}
#endif
if (dispctx->blend_end)
dispctx->blend_end(userdata);
}
XMB_NOINLINE static void xmb_draw_dark_layer(
xmb_handle_t *xmb,
gfx_display_t *p_disp,
gfx_display_ctx_driver_t *dispctx,
void *userdata,
unsigned width,
unsigned height,
float alpha)
{
gfx_display_ctx_draw_t draw;
struct video_coords coords;
float black[16] = {
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
0, 0, 0, 1,
};
draw.x = 0;
draw.y = 0;
draw.width = width;
draw.height = height;
draw.color = &black[0];
draw.vertex = NULL;
draw.matrix_data = NULL;
draw.tex_coord = NULL;
draw.vertex_count = 4;
draw.texture = 0;
draw.pipeline_id = 0;
draw.pipeline_active = false;
if (dispctx->blend_begin)
dispctx->blend_begin(userdata);
gfx_display_draw_bg(p_disp, &draw, &coords, userdata, true, MIN(xmb->alpha, alpha));
if (draw.height > 0 && draw.width > 0)
if (dispctx && dispctx->draw)
dispctx->draw(&draw, userdata, width, height);
if (dispctx->blend_end)
dispctx->blend_end(userdata);
}
static void xmb_draw_no_thumbnail_available(
xmb_handle_t *xmb,
gfx_display_t *p_disp,
void *userdata,
unsigned video_width,
unsigned video_height,
unsigned x_position,
unsigned y_position,
unsigned view_width,
unsigned view_height,
bool shadows_enable,
bool draw_text,
float *color,
math_matrix_4x4 *mymat)
{
gfx_display_ctx_driver_t *dispctx = p_disp->dispctx;
unsigned icon_size = (unsigned)xmb->icon_size;
if (icon_size < 64)
icon_size = 64;
if (icon_size > view_width || icon_size > view_height)
icon_size = MIN(view_width, view_height);
if (draw_text)
y_position -= (icon_size / 5.0f);
if (dispctx)
{
gfx_display_set_alpha(color, xmb->alpha * 0.20f);
if (dispctx->blend_begin)
dispctx->blend_begin(userdata);
if (dispctx->draw)
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
false,
icon_size,
icon_size,
xmb->textures.list[XMB_TEXTURE_IMAGE],
x_position + ((view_width - icon_size) / 2),
y_position + ((view_height - icon_size) / 2) + icon_size,
video_width,
video_height,
xmb->alpha,
0,
1,
&color[0],
xmb->shadow_offset / 2,
mymat);
if (dispctx->blend_end)
dispctx->blend_end(userdata);
}
if (!draw_text)
return;
xmb_draw_text(shadows_enable, xmb, config_get_ptr(),
msg_hash_to_str(MSG_NO_THUMBNAIL_AVAILABLE),
x_position + (view_width / 2),
video_height - y_position - ((view_height - icon_size) / 2),
1, 1, TEXT_ALIGN_CENTER,
video_width, video_height,
xmb->font2);
}
XMB_NOINLINE static void xmb_draw_fullscreen_thumbnails(
xmb_handle_t *xmb,
gfx_animation_t *p_anim,
gfx_display_t *p_disp,
void *userdata,
unsigned video_width,
unsigned video_height,
bool shadows_enable,
unsigned xmb_color_theme,
float *color,
settings_t *settings, size_t selection)
{
static float right_thumbnail_draw_width_prev = 0.0f;
static float right_thumbnail_draw_height_prev = 0.0f;
static float left_thumbnail_draw_width_prev = 0.0f;
static float left_thumbnail_draw_height_prev = 0.0f;
/* Check whether fullscreen thumbnails are visible */
if (xmb->fullscreen_thumbnail_alpha > 0.0f || xmb->want_fullscreen_thumbnails)
{
int header_height;
int header_margin;
int thumbnail_box_width;
int thumbnail_box_height;
int right_thumbnail_x;
int left_thumbnail_x;
int thumbnail_y;
gfx_thumbnail_shadow_t thumbnail_shadow;
gfx_thumbnail_t *right_thumbnail = &xmb->thumbnails.left;
gfx_thumbnail_t *left_thumbnail = &xmb->thumbnails.right;
int view_width = (int)video_width;
int view_height = (int)video_height;
int thumbnail_margin = (int)xmb->icon_size / 2;
int frame_width = (int)xmb->shadow_offset * 2;
uint32_t title_color = 0xFFFFFF00;
uint8_t num_thumbnails = 0;
float right_thumbnail_draw_width = 0.0f;
float right_thumbnail_draw_height = 0.0f;
float left_thumbnail_draw_width = 0.0f;
float left_thumbnail_draw_height = 0.0f;
float background_alpha = 0.95f;
float background_color[16] = {
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
};
float header_alpha = 1.0f;
float header_color[16] = {
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
};
float frame_color[16] = {
0.05f, 0.05f, 0.05f, 1.0f,
0.05f, 0.05f, 0.05f, 1.0f,
0.05f, 0.05f, 0.05f, 1.0f,
0.05f, 0.05f, 0.05f, 1.0f,
};
bool menu_ticker_smooth = settings->bools.menu_ticker_smooth;
enum gfx_animation_ticker_type menu_ticker_type
= (enum gfx_animation_ticker_type)settings->uints.menu_ticker_type;
bool show_header = *xmb->fullscreen_thumbnail_label;
bool show_right_thumbnail = false;
bool show_left_thumbnail = false;
header_height = show_header ? (int)((float)xmb->font_size * 1.5f) + (frame_width * 4) : 0;
/* Sanity check: Return immediately if the view
* width/height is < 1 */
if ((view_width < 1) || (view_height < 1))
goto error;
/* Get thumbnail pointers */
/* Get number of 'active' thumbnails */
show_right_thumbnail =
( right_thumbnail->status == GFX_THUMBNAIL_STATUS_AVAILABLE
|| right_thumbnail->status == GFX_THUMBNAIL_STATUS_PENDING);
show_left_thumbnail =
( left_thumbnail->status == GFX_THUMBNAIL_STATUS_AVAILABLE
|| left_thumbnail->status == GFX_THUMBNAIL_STATUS_PENDING);
if ( (xmb->is_quick_menu && *xmb->savestate_thumbnail_file_path)
|| (xmb->is_state_slot))
{
left_thumbnail = &xmb->thumbnails.savestate;
show_left_thumbnail = (left_thumbnail->status == GFX_THUMBNAIL_STATUS_AVAILABLE);
show_right_thumbnail = false;
}
if (show_right_thumbnail)
num_thumbnails++;
if (show_left_thumbnail)
num_thumbnails++;
/* Get base thumbnail dimensions + draw positions */
/* > Thumbnail bounding box height + y position
* are fixed */
header_margin = (header_height > thumbnail_margin) ? header_height : thumbnail_margin;
thumbnail_box_height = view_height - header_margin - thumbnail_margin;
thumbnail_y = header_margin;
/* Thumbnail bounding box width and x position
* depend upon number of active thumbnails */
if (num_thumbnails == 2)
{
thumbnail_box_width = (view_width - (thumbnail_margin * 3) - frame_width) >> 1;
left_thumbnail_x = thumbnail_margin;
right_thumbnail_x = left_thumbnail_x + thumbnail_box_width + frame_width + thumbnail_margin;
}
else
{
thumbnail_box_width = view_width - (thumbnail_margin * 2);
left_thumbnail_x = thumbnail_margin;
right_thumbnail_x = left_thumbnail_x;
}
/* Sanity check */
if ( thumbnail_box_width < 1
|| thumbnail_box_height < 1)
goto error;
/* Get thumbnail draw dimensions
* > Note: The following code is a bit awkward, since
* we have to do things in a very specific order
* - i.e. we cannot determine proper thumbnail
* layout until we have thumbnail draw dimensions.
* and we cannot get draw dimensions until we have
* the bounding box dimensions... */
if ( show_right_thumbnail
&& right_thumbnail->status == GFX_THUMBNAIL_STATUS_AVAILABLE)
{
gfx_thumbnail_get_draw_dimensions(right_thumbnail,
thumbnail_box_width, thumbnail_box_height, 1.0f,
&right_thumbnail_draw_width, &right_thumbnail_draw_height);
right_thumbnail_draw_width_prev = right_thumbnail_draw_width;
right_thumbnail_draw_height_prev = right_thumbnail_draw_height;
/* Sanity check */
if ( (right_thumbnail_draw_width <= 0.0f)
|| (right_thumbnail_draw_height <= 0.0f))
show_right_thumbnail = false;
}
else if (right_thumbnail->status == GFX_THUMBNAIL_STATUS_PENDING)
{
right_thumbnail_draw_width = right_thumbnail_draw_width_prev;
right_thumbnail_draw_height = right_thumbnail_draw_height_prev;
}
if ( show_left_thumbnail
&& left_thumbnail->status == GFX_THUMBNAIL_STATUS_AVAILABLE)
{
gfx_thumbnail_get_draw_dimensions(
left_thumbnail,
thumbnail_box_width, thumbnail_box_height, 1.0f,
&left_thumbnail_draw_width, &left_thumbnail_draw_height);
left_thumbnail_draw_width_prev = left_thumbnail_draw_width;
left_thumbnail_draw_height_prev = left_thumbnail_draw_height;
/* Sanity check */
if ( (left_thumbnail_draw_width <= 0.0f)
|| (left_thumbnail_draw_height <= 0.0f))
show_left_thumbnail = false;
}
else if (left_thumbnail->status == GFX_THUMBNAIL_STATUS_PENDING)
{
left_thumbnail_draw_width = left_thumbnail_draw_width_prev;
left_thumbnail_draw_height = left_thumbnail_draw_height_prev;
}
/* Adjust thumbnail draw positions to achieve
* uniform appearance (accounting for actual
* draw dimensions...) */
if (num_thumbnails == 2)
{
int left_padding = (thumbnail_box_width - (int)left_thumbnail_draw_width) >> 1;
int right_padding = (thumbnail_box_width - (int)right_thumbnail_draw_width) >> 1;
/* Move thumbnails as close together as possible,
* and horizontally centre the resultant 'block'
* of images */
left_thumbnail_x += right_padding;
right_thumbnail_x -= left_padding;
}
/* Set colour values */
/* > Background */
gfx_display_set_alpha(
background_color, background_alpha * xmb->fullscreen_thumbnail_alpha);
/* > Header background */
header_color[11] = header_alpha * xmb->fullscreen_thumbnail_alpha;
header_color[15] = header_color[11];
/* > Title text */
title_color |= (unsigned)((255.0f * xmb->fullscreen_thumbnail_alpha) + 0.5f);
/* > Thumbnail frame */
gfx_display_set_alpha(frame_color, xmb->fullscreen_thumbnail_alpha);
/* Darken background */
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
0,
0,
(unsigned)view_width,
(unsigned)view_height,
(unsigned)view_width,
(unsigned)view_height,
background_color,
NULL);
/* Draw header */
if (show_header)
{
/* Background */
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
0,
0,
(unsigned)view_width,
(unsigned)header_height,
(unsigned)view_width,
(unsigned)view_height,
header_color,
NULL);
/* Title text */
if (menu_ticker_smooth)
{
char title_buf[NAME_MAX_LENGTH];
gfx_animation_ctx_ticker_smooth_t ticker_smooth;
int title_x = 0;
unsigned ticker_x_offset = 0;
unsigned ticker_str_width = 0;
title_buf[0] = '\0';
ticker_smooth.idx = p_anim->ticker_pixel_idx;
ticker_smooth.font = xmb->font;
ticker_smooth.font_scale = 1.0f;
ticker_smooth.type_enum = menu_ticker_type;
ticker_smooth.spacer = NULL;
ticker_smooth.x_offset = &ticker_x_offset;
ticker_smooth.dst_str_width = &ticker_str_width;
ticker_smooth.selected = true;
ticker_smooth.field_width = (unsigned)view_width;
ticker_smooth.src_str = xmb->fullscreen_thumbnail_label;
ticker_smooth.dst_str = title_buf;
ticker_smooth.dst_str_len = sizeof(title_buf);
/* If ticker is not active, centre the title text */
if (!gfx_animation_ticker_smooth(&ticker_smooth))
title_x = (view_width - (int)ticker_str_width) >> 1;
title_x += (int)ticker_x_offset;
gfx_display_draw_text(
xmb->font,
title_buf,
title_x,
xmb->font_size * 1.33f,
(unsigned)view_width,
(unsigned)view_height,
title_color,
TEXT_ALIGN_LEFT,
1.0f, false, 0.0f, false);
}
/* Note: The non-smooth ticker is a complete failure
* here, since actual text width is unknown. This
* causes the text to be horizontally offset - we
* cannot fix this.
* All we can do in this case is just draw the text
* as-is, horizontally centred, and if the ends get
* clipped than so be it... */
else
gfx_display_draw_text(
xmb->font,
xmb->fullscreen_thumbnail_label,
view_width >> 1,
xmb->font_size * 1.33f,
(unsigned)view_width,
(unsigned)view_height,
title_color,
TEXT_ALIGN_CENTER,
1.0f, false, 0.0f, false);
}
/* Draw thumbnails */
/* > Configure shadow effect */
/* > Disabled for now since the images already have a background border */
if (0 && shadows_enable)
{
float shadow_offset = xmb->icon_size / 24.0f;
thumbnail_shadow.type = GFX_THUMBNAIL_SHADOW_DROP;
thumbnail_shadow.alpha = GFX_SHADOW_ALPHA;
thumbnail_shadow.drop.x_offset = shadow_offset;
thumbnail_shadow.drop.y_offset = shadow_offset;
}
else
thumbnail_shadow.type = GFX_THUMBNAIL_SHADOW_NONE;
/* > Right */
if (show_right_thumbnail)
{
/* Background */
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
right_thumbnail_x
- frame_width
+ ((thumbnail_box_width - (int)right_thumbnail_draw_width) >> 1),
thumbnail_y
- frame_width
+ ((thumbnail_box_height - (int)right_thumbnail_draw_height) >> 1),
(unsigned)right_thumbnail_draw_width + (frame_width << 1),
(unsigned)right_thumbnail_draw_height + (frame_width << 1),
(unsigned)view_width,
(unsigned)view_height,
frame_color,
NULL);
/* Thumbnail */
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
right_thumbnail,
right_thumbnail_x,
thumbnail_y,
(unsigned)thumbnail_box_width,
(unsigned)thumbnail_box_height,
GFX_THUMBNAIL_ALIGN_CENTRE,
xmb->fullscreen_thumbnail_alpha,
1.0f,
&thumbnail_shadow);
}
/* > Left */
if (show_left_thumbnail)
{
/* Background */
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
left_thumbnail_x
- frame_width
+ ((thumbnail_box_width - (int)left_thumbnail_draw_width) >> 1),
thumbnail_y
- frame_width
+ ((thumbnail_box_height - (int)left_thumbnail_draw_height) >> 1),
(unsigned)left_thumbnail_draw_width + (frame_width << 1),
(unsigned)left_thumbnail_draw_height + (frame_width << 1),
(unsigned)view_width,
(unsigned)view_height,
frame_color,
NULL);
/* Thumbnail */
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
left_thumbnail,
left_thumbnail_x,
thumbnail_y,
(unsigned)thumbnail_box_width,
(unsigned)thumbnail_box_height,
GFX_THUMBNAIL_ALIGN_CENTRE,
xmb->fullscreen_thumbnail_alpha,
1.0f,
&thumbnail_shadow);
}
/* Show "No thumbnail available" */
if ( !show_right_thumbnail
&& !show_left_thumbnail)
xmb_draw_no_thumbnail_available(
xmb,
p_disp,
userdata,
video_width,
video_height,
0,
0,
video_width,
video_height,
shadows_enable,
true,
color,
NULL);
}
return;
error:
/* If fullscreen thumbnails are enabled at
* this point, must disable them immediately... */
if (xmb->show_fullscreen_thumbnails)
xmb_hide_fullscreen_thumbnails(xmb, false);
}
static void xmb_frame(void *data, video_frame_info_t *video_info)
{
math_matrix_4x4 mymat = {{ 0.0f }};
unsigned i;
char title_msg[128];
char msg[1024];
char title_truncated[NAME_MAX_LENGTH];
gfx_thumbnail_shadow_t thumbnail_shadow;
size_t selection = 0;
size_t percent_width = 0;
size_t title_header_max_width = 0;
float left_thumbnail_margin_width = 0.0f;
float right_thumbnail_margin_width = 0.0f;
float thumbnail_margin_height_under = 0.0f;
float thumbnail_margin_height_full = 0.0f;
float left_thumbnail_margin_x = 0.0f;
float right_thumbnail_margin_x = 0.0f;
float pseudo_font_length = 0.0f;
xmb_handle_t *xmb = (xmb_handle_t*)data;
settings_t *settings = config_get_ptr();
float thumbnail_scale_factor = (float)settings->uints.menu_xmb_thumbnail_scale_factor / 100.0f;
bool menu_core_enable = settings->bools.menu_core_enable;
bool show_title_header = settings->bools.menu_xmb_show_title_header;
bool vertical_thumbnails = settings->bools.menu_xmb_vertical_thumbnails;
unsigned vertical_fade_factor = settings->uints.menu_xmb_vertical_fade_factor;
unsigned current_menu_icon = settings->uints.menu_xmb_current_menu_icon;
void *userdata = video_info->userdata;
unsigned video_width = video_info->width;
unsigned video_height = video_info->height;
bool shadows_enable = video_info->xmb_shadows_enable;
float alpha_factor = video_info->xmb_alpha_factor;
bool timedate_enable = video_info->timedate_enable;
bool battery_level_enable = video_info->battery_level_enable;
bool video_fullscreen = video_info->fullscreen;
bool mouse_grabbed = video_info->input_driver_grab_mouse_state;
bool menu_mouse_enable = video_info->menu_mouse_enable;
unsigned color_theme = video_info->xmb_color_theme;
bool libretro_running = video_info->libretro_running;
unsigned menu_shader_pipeline = video_info->menu_shader_pipeline;
float menu_wallpaper_opacity = video_info->menu_wallpaper_opacity;
gfx_display_t *p_disp = (gfx_display_t*)video_info->disp_userdata;
gfx_animation_t *p_anim = anim_get_ptr();
gfx_display_ctx_driver_t *dispctx = p_disp->dispctx;
video_driver_state_t *video_st = video_state_get_ptr();
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
bool input_dialog_display_kb = menu_input_dialog_get_display_kb();
bool render_background = false;
bool draw_caret = false;
bool icon_thumbnail_drawn = false;
if (!xmb)
return;
/* Cache the per-frame size on the handle so non-render paths
* can read it without locking video_st. */
xmb->last_width = video_width;
xmb->last_height = video_height;
/* Snapshot context generation — if xmb_context_destroy()
* runs on the main thread while we are mid-render on the
* video thread, the generation will change and we must
* abandon the frame to avoid use-after-free on freed
* textures and fonts. */
{
uint32_t ctx_gen = xmb->context_generation;
/* Per-frame scratch color arrays — local to this stack frame
* to avoid data races with the GPU driver thread.
* coord_black: zero RGB, alpha set per use
* coord_white: one RGB, alpha set per use
* xmb_item_color: base color from theme (black or white), alpha set per use */
{
float coord_black[16] = {0};
float coord_white[16];
float xmb_item_color[16];
memcpy(coord_white, xmb_color_white, sizeof(coord_white));
memcpy(xmb_item_color, xmb->item_color_base, sizeof(xmb_item_color));
/* Snapshot texture handles — the main thread may write new
* handles via async icon load callbacks or context reset at
* any time. Copy the entire array once and use the snapshot
* for all rendering this frame, eliminating torn reads and
* use-after-free on handles freed by context destroy. */
{
uintptr_t tex_list[XMB_TEXTURE_LAST];
uintptr_t tex_bg = xmb->textures.bg;
memcpy(tex_list, xmb->textures.list, sizeof(tex_list));
if (tex_list[XMB_TEXTURE_MAIN_MENU])
xmb->main_menu_node.icon = tex_list[XMB_TEXTURE_MAIN_MENU];
if (tex_list[XMB_TEXTURE_SETTINGS])
xmb->settings_tab_node.icon = tex_list[XMB_TEXTURE_SETTINGS];
if (tex_list[XMB_TEXTURE_HISTORY])
xmb->history_tab_node.icon = tex_list[XMB_TEXTURE_HISTORY];
if (tex_list[XMB_TEXTURE_FAVORITES])
xmb->favorites_tab_node.icon = tex_list[XMB_TEXTURE_FAVORITES];
#ifdef HAVE_IMAGEVIEWER
if (tex_list[XMB_TEXTURE_IMAGES])
xmb->images_tab_node.icon = tex_list[XMB_TEXTURE_IMAGES];
#endif
if (tex_list[XMB_TEXTURE_MUSICS])
xmb->music_tab_node.icon = tex_list[XMB_TEXTURE_MUSICS];
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
if (tex_list[XMB_TEXTURE_MOVIES])
xmb->video_tab_node.icon = tex_list[XMB_TEXTURE_MOVIES];
#endif
if (tex_list[XMB_TEXTURE_ADD])
xmb->add_tab_node.icon = tex_list[XMB_TEXTURE_ADD];
if (tex_list[XMB_TEXTURE_CORE])
xmb->contentless_cores_tab_node.icon = tex_list[XMB_TEXTURE_CORE];
#if defined(HAVE_LIBRETRODB)
if (tex_list[XMB_TEXTURE_RDB])
xmb->explore_tab_node.icon = tex_list[XMB_TEXTURE_RDB];
#endif
#ifdef HAVE_NETWORKING
if (tex_list[XMB_TEXTURE_NETPLAY])
xmb->netplay_tab_node.icon = tex_list[XMB_TEXTURE_NETPLAY];
#endif
msg[0] = '\0';
title_msg[0] = '\0';
title_truncated[0] = '\0';
/* If menu screensaver is active, draw
* screensaver and return */
if (xmb->show_screensaver)
{
menu_screensaver_frame(xmb->screensaver, video_info, p_disp);
return;
}
if (video_st->current_video && video_st->current_video->set_viewport)
video_st->current_video->set_viewport(
video_st->data, video_width, video_height, true, false);
pseudo_font_length = xmb->icon_spacing_horizontal * 4 - xmb->icon_size / 4.0f;
left_thumbnail_margin_width = floorf(xmb->icon_size * (xmb->use_ps3_layout ? 3.3f : 2.0f));
right_thumbnail_margin_width =
(float)video_width - (xmb->icon_size / 2.0f) -
(xmb->margins_screen_left * xmb->scale_mod[5]) -
xmb->icon_spacing_horizontal - pseudo_font_length;
thumbnail_margin_height_under = (float)video_height
- xmb->margins_screen_top - xmb->icon_size;
thumbnail_margin_height_full = (float)video_height - xmb->margins_title_top
- ((xmb->icon_size / 4.0f) * 2.0f);
left_thumbnail_margin_x = xmb->icon_size / 4.0f;
right_thumbnail_margin_x = (float)video_width - left_thumbnail_margin_x
- right_thumbnail_margin_width;
xmb->margins_title = (float)settings->ints.menu_xmb_title_margin * 10.0f;
xmb->margins_title_horizontal_offset = (float)settings->ints.menu_xmb_title_margin_horizontal_offset * 10.0f;
/* Configure shadow effect */
if (shadows_enable)
{
thumbnail_shadow.type = GFX_THUMBNAIL_SHADOW_OUTLINE;
thumbnail_shadow.alpha = 0.2f;
thumbnail_shadow.outline.width = xmb->last_scale_factor;
if (thumbnail_shadow.outline.width < 1)
thumbnail_shadow.outline.width = 1;
}
else
thumbnail_shadow.type = GFX_THUMBNAIL_SHADOW_NONE;
/* Guard: bail if context was destroyed after we started */
if (ctx_gen != xmb->context_generation)
goto ctx_destroyed;
font_driver_bind_block(xmb->font, &xmb->raster_block);
font_driver_bind_block(xmb->font2, &xmb->raster_block2);
xmb->raster_block.carr.coords.vertices = 0;
xmb->raster_block2.carr.coords.vertices = 0;
/* Single-click playlist button hold delay */
if (xmb->alpha_list == 0.0f && xmb->draw_entry_delay)
{
xmb->draw_entry_delay--;
if (!xmb->draw_entry_delay)
xmb_animation_list_alpha(xmb, true);
}
/* Blank dummy core output */
if (!libretro_running)
{
gfx_display_set_alpha(coord_black, 1.0f);
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
0,
0,
video_width,
video_height,
video_width,
video_height,
coord_black,
NULL);
}
gfx_display_set_alpha(coord_black, MIN((float)alpha_factor / 100, xmb->alpha));
gfx_display_set_alpha(coord_white, xmb->alpha);
if (dispctx)
xmb_draw_bg(
userdata,
p_disp,
dispctx,
video_width,
video_height,
menu_shader_pipeline,
color_theme,
MIN(xmb->alpha, menu_wallpaper_opacity),
libretro_running,
MIN(xmb->alpha, alpha_factor / 100),
tex_bg,
xmb->bg_file_path,
coord_black,
coord_white);
selection = menu_st->selection_ptr;
if (dispctx && !dispctx->handles_transform)
{
float cosine = 1.0f; /* cos(rad) = cos(0) = 1.0f */
float sine = 0.0f; /* sine(rad) = sine(0) = 0.0f */
gfx_display_rotate_z(p_disp, &mymat, cosine, sine, userdata);
}
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
/* List icons */
xmb_draw_items(
userdata,
p_disp,
dispctx,
p_anim,
menu_st,
settings,
video_width,
video_height,
shadows_enable,
xmb,
selection_buf,
selection,
xmb->categories_selection_ptr,
&xmb_item_color[0],
&mymat);
/* Show icon thumbnail instead if enabled */
if ( (xmb->is_quick_menu || xmb->is_playlist)
&& settings->uints.menu_icon_thumbnails)
{
gfx_thumbnail_t *icon_thumbnail = &xmb->thumbnails.icon;
bool show_icon_thumbnail =
gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_ICON)
&& icon_thumbnail->status != GFX_THUMBNAIL_STATUS_MISSING;
icon_thumbnail_drawn = show_icon_thumbnail;
if (show_icon_thumbnail)
{
size_t selection = menu_st->selection_ptr;
xmb_node_t *node = NULL;
float gfx_icon_x_margin = left_thumbnail_margin_x * (xmb->use_ps3_layout ? 1 : 0);
float gfx_icon_x = gfx_icon_x_margin;
float gfx_icon_y = xmb->margins_screen_top + (xmb->icon_size / 1.5f);
float gfx_icon_width = left_thumbnail_margin_width;
float gfx_icon_height = xmb->icon_size * (xmb->use_ps3_layout ? 2 : 1.2f);
float gfx_icon_width_draw = 0;
float gfx_icon_height_draw = 0;
float playlist_extra_x = 0;
/* The selection can be stale against a list that was just
* rebuilt smaller, and freshly inserted entries may not
* carry a node yet; without one the thumbnail simply keeps
* the margin position. */
if (selection_buf && selection < selection_buf->size)
node = (xmb_node_t*)selection_buf->list[selection].userdata;
gfx_thumbnail_get_draw_dimensions(
icon_thumbnail,
gfx_icon_width, gfx_icon_height, 1.0f,
&gfx_icon_width_draw, &gfx_icon_height_draw);
playlist_extra_x = xmb->icon_size + fabs(gfx_icon_width - gfx_icon_width_draw) / 2;
if (xmb->is_playlist)
gfx_icon_x += playlist_extra_x;
if (node)
gfx_icon_x += node->x;
if (gfx_icon_x < gfx_icon_x_margin)
gfx_icon_x = gfx_icon_x_margin;
else if (gfx_icon_x > gfx_icon_x_margin + playlist_extra_x)
gfx_icon_x = gfx_icon_x_margin + playlist_extra_x;
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
icon_thumbnail,
gfx_icon_x,
gfx_icon_y,
gfx_icon_width,
gfx_icon_height,
GFX_THUMBNAIL_ALIGN_CENTRE,
1.0f,
1.0f,
&thumbnail_shadow);
}
}
if (dispctx && dispctx->blend_end)
dispctx->blend_end(userdata);
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
/* Current menu icon + arrow */
gfx_display_set_alpha(xmb_item_color, xmb->alpha * xmb->alpha_list);
if (current_menu_icon == XMB_CURRENT_MENU_ICON_NONE)
xmb->current_menu_icon = 0;
if ( !xmb->assets_missing
&& !(icon_thumbnail_drawn && current_menu_icon == XMB_CURRENT_MENU_ICON_NORMAL)
&& xmb->current_menu_icon
&& current_menu_icon)
{
int icon_size = xmb->icon_size;
int current_x = xmb->x
+ xmb->margins_screen_left
+ xmb->icon_spacing_horizontal
- (icon_size / 2.0f);
int current_y = xmb->margins_screen_top
+ (icon_size / 2.0f)
+ (xmb->icon_spacing_vertical * xmb->active_item_factor);
if (current_menu_icon == XMB_CURRENT_MENU_ICON_TITLE)
{
float scale_factor = xmb->scale_cap;
icon_size /= 3.5f;
if (scale_factor > 1)
icon_size *= scale_factor;
current_x = xmb->margins_title_left - (icon_size / 0.75f);
current_x += (xmb->icon_size - icon_size) / 2.0f;
if (current_x < icon_size / 2)
current_x = icon_size / 2;
current_y = xmb->margins_title_top + (icon_size / 6.0f);
}
if (xmb->depth > 1 || current_menu_icon == XMB_CURRENT_MENU_ICON_TITLE)
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
shadows_enable,
icon_size,
icon_size,
xmb->current_menu_icon,
current_x,
current_y,
video_width,
video_height,
xmb->alpha,
0,
1,
&xmb_item_color[0],
xmb->shadow_offset,
&mymat);
gfx_display_set_alpha(xmb_item_color, MIN(xmb->items_passive_alpha, xmb->alpha));
if (xmb->depth > 1 && current_menu_icon == XMB_CURRENT_MENU_ICON_NORMAL)
{
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
shadows_enable,
xmb->icon_size,
xmb->icon_size,
tex_list[XMB_TEXTURE_ARROW],
current_x + (xmb->use_ps3_layout ? (icon_size * 1.1f) : (icon_size * 0.70f)),
current_y,
video_width,
video_height,
xmb->alpha,
0,
0.5f,
&xmb_item_color[0],
xmb->shadow_offset / 2,
&mymat);
}
}
if (dispctx && dispctx->blend_end)
dispctx->blend_end(userdata);
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
/* Horizontal tab icons */
if (!xmb->assets_missing && xmb->show_horizontal_list)
{
unsigned horizontal_list_size = (xmb->show_playlist_tabs)
? (unsigned)xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL)
: 0;
for (i = 0; i <= xmb->system_tab_end + horizontal_list_size; i++)
{
xmb_node_t *node = xmb_get_node(xmb, i);
if (!node)
continue;
gfx_display_set_alpha(xmb_item_color, MIN(node->alpha, xmb->alpha));
if (xmb_item_color[3] != 0)
{
uintptr_t texture = node->icon;
float x;
float y;
float scale_factor;
/* Fallback for nodes whose icon hasn't resolved yet
* (e.g. .lvw nodes when CURSOR wasn't loaded during
* list build, or .lpl nodes with an in-flight async
* load). Use the shared CURSOR texture for this frame
* only — do NOT write it back into node->icon, or
* xmb_context_destroy_horizontal_list will later
* double-free it when it unloads the node's icon
* alongside tex_list[CURSOR]. */
if (!texture && tex_list[XMB_TEXTURE_CURSOR])
texture = tex_list[XMB_TEXTURE_CURSOR];
x = xmb->x + xmb->categories_x_pos
+ xmb->margins_screen_left
+ xmb->icon_spacing_horizontal * (i + 1)
- xmb->icon_size / 2.0;
y = xmb->margins_screen_top
+ xmb->icon_size / 2.0;
scale_factor = node->zoom;
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
shadows_enable,
xmb->icon_size,
xmb->icon_size,
texture,
x,
y,
video_width,
video_height,
xmb->alpha,
0,
scale_factor,
&xmb_item_color[0],
xmb->shadow_offset,
&mymat);
}
}
}
/**************************/
/* Draw thumbnails: START */
/**************************/
/* Reset thumbnails when starting/closing content */
if (xmb->libretro_running != libretro_running)
{
xmb->libretro_running = libretro_running;
if (xmb->is_quick_menu && libretro_running)
xmb->fullscreen_thumbnails_available = false;
else if (xmb->is_quick_menu && !libretro_running)
xmb->fullscreen_thumbnails_available = true;
}
/* Allow browsing playlist in fullscreen thumbnail mode */
if ( (xmb->is_playlist || xmb->is_explore_list || xmb->is_state_slot)
&& (xmb->show_fullscreen_thumbnails)
&& (menu_st->selection_ptr != xmb->fullscreen_thumbnail_selection))
xmb_show_fullscreen_thumbnails(xmb, menu_st, menu_st->selection_ptr);
else if (!xmb->show_fullscreen_thumbnails
&& xmb->want_fullscreen_thumbnails)
xmb_show_fullscreen_thumbnails(xmb, menu_st, menu_st->selection_ptr);
/* Save state thumbnail, right side in PS3 layout, left in PSP layout */
if ( (xmb->is_quick_menu || xmb->is_state_slot)
&& *xmb->savestate_thumbnail_file_path
&& (!xmb->show_fullscreen_thumbnails)
)
{
float background_color[16] = {
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
};
float thumb_width = right_thumbnail_margin_width;
float thumb_height = thumb_width - (thumb_width / 4);
float scaled_thumb_width = thumb_width * thumbnail_scale_factor;
float scaled_thumb_height = thumb_height * thumbnail_scale_factor;
float thumb_x = right_thumbnail_margin_x + ((thumb_width - scaled_thumb_width) / 2.0f);
float thumb_y = (video_height - thumb_height) / 2.0f;
/* Make sure thumbnail stays visible with extra wide screens */
if (thumb_height > video_height)
thumb_y -= (thumb_height - video_height) / 2.0f;
if (!xmb->use_ps3_layout)
{
float height_offset = xmb->icon_size * 0.35f;
thumb_width = left_thumbnail_margin_width;
thumb_height = thumb_width - (thumb_width / 4);
scaled_thumb_width = thumb_width * thumbnail_scale_factor;
scaled_thumb_height = thumb_height * thumbnail_scale_factor;
thumb_x = (thumb_width - scaled_thumb_width) / 2.0f;
thumb_y = video_height - thumbnail_margin_height_under - height_offset;
}
if ( (xmb->thumbnails.savestate.status == GFX_THUMBNAIL_STATUS_AVAILABLE)
|| (xmb->thumbnails.savestate.status == GFX_THUMBNAIL_STATUS_PENDING))
{
if (settings->bools.menu_thumbnail_background_enable)
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
&xmb->thumbnails.savestate,
thumb_x,
thumb_y,
scaled_thumb_width > 0.0f ? (unsigned)scaled_thumb_width : 0,
scaled_thumb_height > 0.0f ? (unsigned)scaled_thumb_height : 0,
GFX_THUMBNAIL_ALIGN_CENTRE,
1.0f, 1.0f, &thumbnail_shadow);
}
else
{
/* Darken background */
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
if (!(xmb->thumbnails.savestate.flags & GFX_THUMB_FLAG_BG_ONLY))
xmb_draw_no_thumbnail_available(
xmb,
p_disp,
userdata,
video_width,
video_height,
thumb_x,
thumb_y,
scaled_thumb_width,
scaled_thumb_height,
shadows_enable,
false,
xmb_item_color,
NULL);
}
}
else if (xmb->fullscreen_thumbnails_available && !xmb->show_fullscreen_thumbnails)
{
float background_color[16] = {
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
0.0f, 0.0f, 0.0f, 1.0f,
};
bool thumbnail_background = settings->bools.menu_thumbnail_background_enable;
bool show_right_thumbnail =
(gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT))
&& xmb->show_thumbnails
&& ( (xmb->thumbnails.right.status == GFX_THUMBNAIL_STATUS_AVAILABLE)
|| ( xmb->thumbnails.right.status < GFX_THUMBNAIL_STATUS_AVAILABLE
&& xmb->thumbnails_right_status_prev <= GFX_THUMBNAIL_STATUS_AVAILABLE
&& xmb->thumbnails_right_status_prev != GFX_THUMBNAIL_STATUS_UNKNOWN));
bool show_left_thumbnail =
(gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT))
&& ( (xmb->thumbnails.left.status == GFX_THUMBNAIL_STATUS_AVAILABLE)
|| ( xmb->thumbnails.left.status < GFX_THUMBNAIL_STATUS_AVAILABLE
&& xmb->thumbnails_left_status_prev <= GFX_THUMBNAIL_STATUS_AVAILABLE
&& xmb->thumbnails_left_status_prev != GFX_THUMBNAIL_STATUS_UNKNOWN));
/* Check if we are using the proper PS3 layout,
* or the aborted PSP layout */
if (xmb->use_ps3_layout)
{
/* Check if user has selected vertically
* stacked thumbnails */
if (vertical_thumbnails)
{
if (!xmb->show_thumbnails)
show_right_thumbnail = show_left_thumbnail = false;
/* Right + left thumbnails, right side */
if (show_right_thumbnail && show_left_thumbnail)
{
float thumb_width = right_thumbnail_margin_width;
float thumb_height = (thumbnail_margin_height_full
- (xmb->icon_size / 4.0f)) / 2.0f;
float scaled_thumb_width = thumb_width * thumbnail_scale_factor;
float scaled_thumb_height = thumb_height * thumbnail_scale_factor;
float thumb_x = right_thumbnail_margin_x
+ ((thumb_width - scaled_thumb_width) / 2.0f);
float thumb_y_base = xmb->margins_title_top
+ (xmb->icon_size / 4.0f);
float thumb_y_offset = (thumb_height - scaled_thumb_height) / 2.0f;
float right_thumb_y = thumb_y_base + thumb_y_offset;
float left_thumb_y = thumb_y_base + thumb_height + (xmb->icon_size / 8) + thumb_y_offset;
/* Darken background */
if (thumbnail_background)
{
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
right_thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
left_thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
}
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
&xmb->thumbnails.right,
thumb_x,
right_thumb_y,
(scaled_thumb_width > 0.0f) ? (unsigned)scaled_thumb_width : 0,
(scaled_thumb_height > 0.0f) ? (unsigned)scaled_thumb_height : 0,
GFX_THUMBNAIL_ALIGN_CENTRE,
1.0f, 1.0f, &thumbnail_shadow);
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
&xmb->thumbnails.left,
thumb_x,
left_thumb_y,
(scaled_thumb_width > 0.0f) ? (unsigned)scaled_thumb_width : 0,
(scaled_thumb_height > 0.0f) ? (unsigned)scaled_thumb_height : 0,
GFX_THUMBNAIL_ALIGN_CENTRE,
1.0f, 1.0f, &thumbnail_shadow);
}
/* Right *or* left, right side */
else if (show_right_thumbnail || show_left_thumbnail)
{
float thumb_width = right_thumbnail_margin_width;
float thumb_height = thumbnail_margin_height_under;
float scaled_thumb_width = thumb_width * thumbnail_scale_factor;
float scaled_thumb_height = thumb_height * thumbnail_scale_factor;
float thumb_x = right_thumbnail_margin_x
+ ((thumb_width - scaled_thumb_width) / 2.0f);
float thumb_y = xmb->margins_screen_top + (xmb->icon_size / 1.5f);
if (thumbnail_background)
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
(show_right_thumbnail) ? &xmb->thumbnails.right : &xmb->thumbnails.left,
thumb_x,
thumb_y,
(scaled_thumb_width > 0.0f) ? (unsigned)scaled_thumb_width : 0,
(scaled_thumb_height > 0.0f) ? (unsigned)scaled_thumb_height : 0,
(thumbnail_background) ? GFX_THUMBNAIL_ALIGN_CENTRE : GFX_THUMBNAIL_ALIGN_TOP,
1.0f, 1.0f, &thumbnail_shadow);
}
}
else
{
/* Right thumbnail, right side */
if (show_right_thumbnail)
{
float thumb_width = right_thumbnail_margin_width;
float thumb_height = thumbnail_margin_height_under;
float scaled_thumb_width = thumb_width * thumbnail_scale_factor;
float scaled_thumb_height = thumb_height * thumbnail_scale_factor;
float thumb_x = right_thumbnail_margin_x
+ ((thumb_width - scaled_thumb_width) / 2.0f);
float thumb_y = xmb->margins_screen_top + (xmb->icon_size / 1.5f);
if (thumbnail_background)
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
&xmb->thumbnails.right,
thumb_x,
thumb_y,
(scaled_thumb_width > 0.0f) ? (unsigned)scaled_thumb_width : 0,
(scaled_thumb_height > 0.0f) ? (unsigned)scaled_thumb_height : 0,
(thumbnail_background) ? GFX_THUMBNAIL_ALIGN_CENTRE : GFX_THUMBNAIL_ALIGN_TOP,
1.0f, 1.0f, &thumbnail_shadow);
}
/* Left thumbnail, left side */
if (show_left_thumbnail)
{
bool icon_thumbnails = settings->uints.menu_icon_thumbnails;
uint8_t current_menu_icon = settings->uints.menu_xmb_current_menu_icon;
float margins_title_min = -(xmb->icon_size / 5.0f);
float margins_title = (xmb->margins_title_bottom < margins_title_min)
? margins_title_min : xmb->margins_title_bottom;
float height_offset = (current_menu_icon != XMB_CURRENT_MENU_ICON_NORMAL && !icon_thumbnails)
? 0 : xmb->icon_size * (icon_thumbnails ? 2.05f : 1.5f);
float thumb_width = left_thumbnail_margin_width;
float thumb_height = thumbnail_margin_height_under - margins_title - height_offset;
float scaled_thumb_width = thumb_width * thumbnail_scale_factor;
float scaled_thumb_height = thumb_height * thumbnail_scale_factor;
float thumb_x = left_thumbnail_margin_x
+ ((thumb_width - scaled_thumb_width) / 2.0f);
float thumb_y = xmb->margins_screen_top + height_offset + (xmb->icon_size / 1.5f);
if (thumb_height > xmb->icon_size)
{
if (thumbnail_background)
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
&xmb->thumbnails.left,
thumb_x,
thumb_y,
(scaled_thumb_width > 0.0f) ? (unsigned)scaled_thumb_width : 0,
(scaled_thumb_height > 0.0f) ? (unsigned)scaled_thumb_height : 0,
(thumbnail_background) ? GFX_THUMBNAIL_ALIGN_CENTRE : GFX_THUMBNAIL_ALIGN_TOP,
1.0f, 1.0f, &thumbnail_shadow);
}
}
}
}
/* This is the PSP layout - thumbnails are only
* drawn on the left hand side
* > If left thumbnail is available, show it
* > If not, show right thumbnail instead
* (if available) */
else if (show_right_thumbnail || show_left_thumbnail)
{
bool icon_thumbnails = settings->uints.menu_icon_thumbnails;
uint8_t current_menu_icon = settings->uints.menu_xmb_current_menu_icon;
float margins_title_min = -(xmb->icon_size / 5.0f);
float margins_title = (xmb->margins_title_bottom < margins_title_min)
? margins_title_min : xmb->margins_title_bottom;
float height_offset = (current_menu_icon != XMB_CURRENT_MENU_ICON_NORMAL && !icon_thumbnails)
? 0 : xmb->icon_size * (icon_thumbnails ? 1.25f : 1.25f);
float thumb_width = left_thumbnail_margin_width;
float thumb_height = thumbnail_margin_height_under - margins_title - height_offset;
float scaled_thumb_width = thumb_width * thumbnail_scale_factor;
float scaled_thumb_height = thumb_height * thumbnail_scale_factor;
float thumb_x = (thumb_width - scaled_thumb_width) / 2.0f;
float thumb_y = xmb->margins_screen_top + height_offset + (xmb->icon_size / 1.5f);
/* Very small thumbnails look ridiculous
* > Impose a minimum size limit */
if (thumb_height > xmb->icon_size)
{
if (thumbnail_background)
gfx_display_draw_quad(
p_disp,
userdata,
video_width,
video_height,
thumb_x,
thumb_y,
scaled_thumb_width,
scaled_thumb_height,
video_width,
video_height,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_width,
video_height,
(show_left_thumbnail) ? &xmb->thumbnails.left : &xmb->thumbnails.right,
thumb_x,
thumb_y,
(scaled_thumb_width > 0.0f) ? (unsigned)scaled_thumb_width : 0,
(scaled_thumb_height > 0.0f) ? (unsigned)scaled_thumb_height : 0,
(thumbnail_background) ? GFX_THUMBNAIL_ALIGN_CENTRE : GFX_THUMBNAIL_ALIGN_TOP,
1.0f, 1.0f, &thumbnail_shadow);
}
}
}
if (dispctx && dispctx->blend_end)
dispctx->blend_end(userdata);
/**************************/
/* Draw thumbnails: END */
/**************************/
/* Battery */
gfx_display_set_alpha(xmb_item_color, MIN(xmb->alpha, 1.00f));
if (battery_level_enable)
{
gfx_display_ctx_powerstate_t powerstate;
char msg[12];
msg[0] = '\0';
menu_display_powerstate(&powerstate, msg, sizeof(msg));
if (powerstate.battery_enabled)
{
float scale_factor = xmb->scale_cap;
float icon_size = (!xmb->assets_missing) ? xmb->icon_size * 0.90f : 0;
size_t x_pos = (float)(icon_size / 4 * scale_factor);
if (!xmb->assets_missing)
{
float margin_offset = -(icon_size / 2) - (7 * xmb->last_scale_factor);
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
shadows_enable,
icon_size,
icon_size,
tex_list[
powerstate.charging ? XMB_TEXTURE_BATTERY_CHARGING :
(powerstate.percent > 80) ? XMB_TEXTURE_BATTERY_FULL :
(powerstate.percent > 60) ? XMB_TEXTURE_BATTERY_80 :
(powerstate.percent > 40) ? XMB_TEXTURE_BATTERY_60 :
(powerstate.percent > 20) ? XMB_TEXTURE_BATTERY_40 :
XMB_TEXTURE_BATTERY_20
],
video_width - xmb->margins_title_left + margin_offset,
icon_size + xmb->margins_title_top + margin_offset,
video_width,
video_height,
xmb->alpha,
0,
scale_factor,
&xmb_item_color[0],
xmb->shadow_offset,
&mymat);
if (dispctx && dispctx->blend_end)
dispctx->blend_end(userdata);
}
percent_width = (unsigned)
font_driver_get_message_width(
xmb->font, msg, strlen(msg), 1.0f);
title_header_max_width = x_pos;
xmb_draw_text(shadows_enable, xmb, settings, msg,
video_width - xmb->margins_title_left - x_pos,
xmb->margins_title_top, 1, 1, TEXT_ALIGN_RIGHT,
video_width, video_height, xmb->font);
}
}
/* Clock */
if (timedate_enable)
{
gfx_display_ctx_datetime_t datetime;
char timedate[256];
size_t _len = 0;
size_t x_pos = 0;
float scale_factor = xmb->scale_cap;
float icon_size = (!xmb->assets_missing) ? xmb->icon_size * 0.90f : 0;
if (percent_width)
{
x_pos += percent_width + (float)(icon_size / 2 * scale_factor);
/* Use font size based padding with missing assets instead of icon size */
if (!icon_size)
x_pos += font_driver_get_message_width(xmb->font, "0", 2, 1.0f);
}
if (!xmb->assets_missing)
{
float margin_offset = -(icon_size / 2) - (7 * xmb->last_scale_factor);
if (dispctx && dispctx->blend_begin)
dispctx->blend_begin(userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_width,
video_height,
shadows_enable,
icon_size,
icon_size,
tex_list[XMB_TEXTURE_CLOCK],
video_width - xmb->margins_title_left + margin_offset - x_pos,
icon_size + xmb->margins_title_top + margin_offset,
video_width,
video_height,
xmb->alpha,
0,
scale_factor,
&xmb_item_color[0],
xmb->shadow_offset,
&mymat);
if (dispctx && dispctx->blend_end)
dispctx->blend_end(userdata);
}
datetime.time_mode = settings->uints.menu_timedate_style;
datetime.date_separator = settings->uints.menu_timedate_date_separator;
_len = menu_display_timedate(&datetime, timedate, sizeof(timedate));
title_header_max_width = x_pos + font_driver_get_message_width(
xmb->font, timedate, _len, 1.0f);
xmb_draw_text(shadows_enable, xmb, settings, timedate,
video_width - xmb->margins_title_left - x_pos
- (!xmb->assets_missing ? xmb->icon_size / 4 * scale_factor : 0),
xmb->margins_title_top, 1, 1, TEXT_ALIGN_RIGHT,
video_width, video_height, xmb->font);
}
/* Use alternative title if available */
strlcpy(title_truncated,
*xmb->title_name_alt
? xmb->title_name_alt
: xmb->title_name,
sizeof(title_truncated));
if (!vertical_fade_factor && selection > 1)
{
/* skip 25 UTF8 multi-byte chars */
char *end = (char*)utf8skip(title_truncated, 25);
*end = '\0';
}
/* Title text */
if (show_title_header)
{
gfx_animation_ctx_ticker_t ticker;
gfx_animation_ctx_ticker_smooth_t ticker_smooth;
char tmp[NAME_MAX_LENGTH];
size_t icon_len =
( !xmb->assets_missing
&& settings->uints.menu_xmb_current_menu_icon == XMB_CURRENT_MENU_ICON_TITLE)
? xmb->icon_size / 2.5f : 0;
size_t tmp_len = (video_width
- xmb->margins_title_left
- xmb->icon_size
- icon_len
- title_header_max_width);
unsigned ticker_x_offset = 0;
bool use_smooth_ticker = settings->bools.menu_ticker_smooth;
enum gfx_animation_ticker_type menu_ticker_type
= (enum gfx_animation_ticker_type)settings->uints.menu_ticker_type;
tmp[0] = '\0';
/* Initial ticker configuration */
if (use_smooth_ticker)
{
ticker_smooth.idx = p_anim->ticker_pixel_idx;
ticker_smooth.font = xmb->font;
ticker_smooth.font_scale = 1.0f;
ticker_smooth.type_enum = menu_ticker_type;
ticker_smooth.spacer = NULL;
ticker_smooth.selected = true;
ticker_smooth.x_offset = &ticker_x_offset;
ticker_smooth.dst_str_width = NULL;
}
else
{
ticker.idx = p_anim->ticker_idx;
ticker.type_enum = menu_ticker_type;
ticker.spacer = NULL;
ticker.selected = true;
}
if (use_smooth_ticker)
{
ticker_smooth.field_width = (unsigned)tmp_len;
ticker_smooth.src_str = title_truncated;
ticker_smooth.dst_str = tmp;
ticker_smooth.dst_str_len = sizeof(tmp);
if (ticker_smooth.src_str)
gfx_animation_ticker_smooth(&ticker_smooth);
}
else
{
unsigned font_width = (unsigned)
font_driver_get_message_width(
xmb->font, "a", 1, 1.0f);
ticker.str = title_truncated;
ticker.s = tmp;
ticker.s_len = sizeof(tmp);
ticker.len = (tmp_len * xmb->last_scale_factor) / font_width;
if (ticker.str)
gfx_animation_ticker(&ticker);
}
xmb_draw_text(shadows_enable, xmb, settings,
tmp,
(float)ticker_x_offset + xmb->margins_title_left + icon_len,
xmb->margins_title_top,
1, 1, TEXT_ALIGN_LEFT,
video_width, video_height, xmb->font);
}
if (menu_core_enable)
{
menu_entries_get_core_title(title_msg, sizeof(title_msg));
xmb_draw_text(shadows_enable, xmb, settings,
title_msg,
xmb->margins_title_left,
video_height - xmb->margins_title_bottom,
1, 1, TEXT_ALIGN_LEFT,
video_width, video_height, xmb->font);
}
/* Guard: bail if context was destroyed during the draw pass */
if (ctx_gen != xmb->context_generation)
goto ctx_destroyed;
/* Restore horizontal animation ability after first toggle */
if (xmb->is_first_frame)
xmb->allow_horizontal_animation = true;
/* First-frame init done — subsequent frames are normal.
* Cleared after the last ctx-destroyed guard so that a context
* death mid-frame leaves the flag set for the retry. */
xmb->is_first_frame = false;
/* Pick up a font rebuilt since the last frame before the next one
* lays out with a stale width. */
xmb_sync_wideglyph(xmb);
if (xmb->font && xmb->font->renderer && xmb->font->renderer->flush)
xmb->font->renderer->flush(video_width,
video_height, xmb->font->renderer_data);
if (xmb->font2 && xmb->font2->renderer && xmb->font2->renderer->flush)
xmb->font2->renderer->flush(video_width,
video_height, xmb->font2->renderer_data);
font_driver_bind_block(xmb->font, NULL);
font_driver_bind_block(xmb->font2, NULL);
/* Draw fullscreen thumbnails, if required */
xmb_draw_fullscreen_thumbnails(
xmb,
p_anim,
p_disp,
userdata,
video_width,
video_height,
shadows_enable,
color_theme,
xmb_item_color,
settings, selection);
if (input_dialog_display_kb)
{
const char *str = menu_input_dialog_get_buffer();
const char *label = menu_st->input_dialog_kb_label;
size_t _len = strlcpy(msg, label, sizeof(msg) - 2);
msg[ _len] = '\n';
msg[++_len] = '\0';
strlcpy(msg + _len,
(str && *str) ? str : " ",
sizeof(msg) - _len);
render_background = true;
draw_caret = true;
}
if (xmb->box_message && *xmb->box_message)
{
strlcpy(msg, xmb->box_message, sizeof(msg));
free(xmb->box_message);
xmb->box_message = NULL;
render_background = true;
draw_caret = false;
}
if (render_background)
{
if (dispctx)
xmb_draw_dark_layer(xmb, p_disp, dispctx,
userdata, video_width, video_height,
(input_dialog_display_kb) ? 0.95f : 0.75f);
if (xmb->font && *msg)
xmb_render_messagebox_internal(userdata, p_disp,
dispctx,
video_width, video_height,
xmb, msg, draw_caret, &mymat);
}
/* Cursor image */
if (xmb->show_mouse)
{
bool cursor_visible = menu_mouse_enable
&& (video_fullscreen || mouse_grabbed);
gfx_display_set_alpha(coord_white, MIN(xmb->alpha, 1.00f));
if (cursor_visible)
gfx_display_draw_cursor(
p_disp,
userdata,
video_width,
video_height,
cursor_visible,
&coord_white[0],
xmb->cursor_size,
tex_list[XMB_TEXTURE_POINTER],
xmb->pointer.x,
xmb->pointer.y,
video_width,
video_height);
}
if (video_st->current_video && video_st->current_video->set_viewport)
video_st->current_video->set_viewport(
video_st->data, video_width, video_height, false, true);
ctx_destroyed:
; /* no-op — reached if context was destroyed mid-frame */
} /* end of texture snapshot scope */
} /* end of local color array scope */
} /* end of context generation scope */
}
static void xmb_ribbon_set_vertex(float *ribbon_verts,
unsigned idx, unsigned row, unsigned col)
{
ribbon_verts[idx++] = ((float)col) / (XMB_RIBBON_COLS - 1) * 2.0f - 1.0f;
ribbon_verts[idx++] = ((float)row) / (XMB_RIBBON_ROWS - 1) * 2.0f - 1.0f;
}
static void xmb_init_ribbon(xmb_handle_t * xmb)
{
video_coords_t coords;
unsigned r, c, col;
unsigned i = 0;
gfx_display_t *p_disp = disp_get_ptr();
video_coord_array_t *ca = &p_disp->dispca;
unsigned vertices_total = XMB_RIBBON_VERTICES;
float *ribbon_verts = (float*)calloc(2 * vertices_total, sizeof(float));
/* NULL-check the calloc: the for-loop below unconditionally writes
* into ribbon_verts via xmb_ribbon_set_vertex. Skip ribbon init
* entirely on OOM - the ribbon is a decorative background
* animation; its absence is visually degraded but not
* functionally broken. */
if (!ribbon_verts)
return;
/* Set up vertices */
for (r = 0; r < XMB_RIBBON_ROWS - 1; r++)
{
for (c = 0; c < XMB_RIBBON_COLS; c++)
{
col = r % 2 ? XMB_RIBBON_COLS - c - 1 : c;
xmb_ribbon_set_vertex(ribbon_verts, i, r, col);
xmb_ribbon_set_vertex(ribbon_verts, i + 2, r + 1, col);
i += 4;
}
}
/* The ribbon vertex shaders declare exactly one attribute --
* "in vec3 VertexCoord" in modern_pipeline_xmb_ribbon.glsl.vert.h,
* "attribute vec3 VertexCoord" in the legacy one -- and every
* shader backend binds a stream only when its attribute location
* is >= 0, so colour, texture and LUT coordinates are never read
* back out of this array. They used to be supplied anyway, as one
* calloc()ed buffer of zeros passed three times, because
* video_coord_array_append() copied all four streams
* unconditionally. It no longer does. */
coords.color = NULL;
coords.vertex = ribbon_verts;
coords.tex_coord = NULL;
coords.lut_tex_coord = NULL;
coords.vertices = vertices_total;
video_coord_array_append(ca, &coords, coords.vertices);
free(ribbon_verts);
}
static void xmb_menu_animation_update_time(
float *ticker_pixel_increment,
unsigned video_width, unsigned video_height)
{
xmb_handle_t *xmb = NULL;
menu_handle_t *menu = menu_state_get_ptr()->driver_data;
if (menu)
if ((xmb = (xmb_handle_t*)menu->userdata))
*(ticker_pixel_increment) *= xmb->last_scale_factor;
}
static void *xmb_init(void **userdata, bool video_is_threaded)
{
int i;
unsigned width, height;
xmb_handle_t *xmb = NULL;
settings_t *settings = config_get_ptr();
gfx_animation_t *p_anim = anim_get_ptr();
gfx_display_t *p_disp = disp_get_ptr();
menu_handle_t *menu = (menu_handle_t*)calloc(1, sizeof(*menu));
struct menu_state *menu_st = menu_state_get_ptr();
if (!menu)
return NULL;
video_driver_get_output_size(&width, &height);
if (!(xmb = (xmb_handle_t*)calloc(1, sizeof(xmb_handle_t))))
{
free(menu);
return NULL;
}
/* Initialise last_{width,height} from the snapshot taken
* above; xmb_frame will refresh these every render. */
xmb->last_width = width;
xmb->last_height = height;
xmb_init_scale_mod(xmb->scale_mod, settings->floats.menu_scale_factor * 100.0f);
xmb->scale_cap = (settings->floats.menu_scale_factor > 1.0f)
? settings->floats.menu_scale_factor
: 1.0f;
*userdata = xmb;
xmb->categories_active_idx = 0;
xmb->categories_active_idx_old = 0;
xmb->x = 0;
xmb->categories_x_pos = 0;
xmb->depth = 1;
xmb->old_depth = 1;
xmb->alpha = 0.0f;
xmb->alpha_list = 1.0f;
xmb->is_first_frame = true;
xmb_refresh_system_tabs_list(xmb);
for (i = 0; i < XMB_TAB_MAX_LENGTH; i++)
xmb->tab_selection[i] = 0;
menu_st->flags &= ~MENU_ST_FLAG_PREVENT_POPULATE;
/* TODO/FIXME - we don't use framebuffer at all
* for XMB, we should refactor this dependency
* away. */
p_disp->framebuf_width = width;
p_disp->framebuf_height = height;
gfx_display_init_white_texture();
xmb->horizontal_list.list = NULL;
xmb->horizontal_list.capacity = 0;
xmb->horizontal_list.size = 0;
xmb_init_horizontal_list(xmb);
xmb_init_ribbon(xmb);
/* Initialise screensaver */
if (!(xmb->screensaver = menu_screensaver_init()))
goto error;
xmb->fullscreen_thumbnails_available = false;
xmb->show_fullscreen_thumbnails = false;
xmb->skip_thumbnail_reset = false;
xmb->show_thumbnails = true;
xmb->fullscreen_thumbnail_alpha = 0.0f;
xmb->fullscreen_thumbnail_selection = 0;
xmb->fullscreen_thumbnail_label[0] = '\0';
xmb->savestate_thumbnail_file_path[0] = '\0';
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
gfx_thumbnail_set_stream_delay(-1.0f);
gfx_thumbnail_set_fade_duration(-1.0f);
gfx_thumbnail_set_fade_missing(false);
xmb->use_ps3_layout =
xmb_use_ps3_layout(settings->uints.menu_xmb_layout, width, height);
xmb->last_use_ps3_layout = xmb->use_ps3_layout;
xmb->last_scale_factor = xmb_get_scale_factor(
settings->floats.menu_scale_factor,
xmb->use_ps3_layout, width);
xmb->margins_title = (float)settings->ints.menu_xmb_title_margin * 10.0f;
xmb->last_margins_title = xmb->margins_title;
xmb->margins_title_horizontal_offset = (float)settings->ints.menu_xmb_title_margin_horizontal_offset * 10.0f;
xmb->last_margins_title_horizontal_offset = xmb->margins_title_horizontal_offset;
p_anim->updatetime_cb = xmb_menu_animation_update_time;
/* set word_wrap function pointer */
xmb->word_wrap =
msg_hash_get_wideglyph_str() ? word_wrap_wideglyph : word_wrap;
return menu;
error:
free(menu);
/* See comment in xmb_refresh_horizontal_list: the map's
* values are non-owning pointers into the horizontal_list,
* so it must be freed before the nodes are. */
RHMAP_FREE(xmb->playlist_db_node_map);
xmb_free_list_nodes(&xmb->horizontal_list, false);
file_list_deinitialize(&xmb->horizontal_list);
return NULL;
}
static void xmb_free(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (xmb)
{
/* Invalidate any in-flight async icon loads before freeing
* the nodes they would write into */
xmb_icon_load_gen++;
xmb_ctx_icon_load_gen++;
/* See comment in xmb_refresh_horizontal_list: free the
* db_node_map before the nodes its values point into. */
RHMAP_FREE(xmb->playlist_db_node_map);
xmb_free_list_nodes(&xmb->horizontal_list, false);
file_list_deinitialize(&xmb->horizontal_list);
video_coord_array_free(&xmb->raster_block.carr);
video_coord_array_free(&xmb->raster_block2.carr);
if (xmb->box_message)
free(xmb->box_message);
if (xmb->bg_file_path)
free(xmb->bg_file_path);
free(xmb->wrap_scratch);
menu_screensaver_free(xmb->screensaver);
}
gfx_display_deinit_white_texture();
}
static void xmb_context_bg_destroy(xmb_handle_t *xmb)
{
if (!xmb)
return;
video_driver_texture_unload(&xmb->textures.bg);
gfx_display_deinit_white_texture();
}
static bool xmb_load_image(void *userdata, void *data,
enum menu_image_type type)
{
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
if (!xmb)
return false;
switch (type)
{
case MENU_IMAGE_WALLPAPER:
xmb_context_bg_destroy(xmb);
video_driver_texture_load(data,
gfx_display_texture_filter(),
&xmb->textures.bg);
gfx_display_init_white_texture();
break;
case MENU_IMAGE_NONE:
default:
xmb_context_bg_destroy(xmb);
gfx_display_init_white_texture();
break;
}
return true;
}
static void xmb_context_reset(void *data, bool is_threaded)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (xmb)
xmb_context_reset_internal(xmb, is_threaded, true,
config_get_ptr()->uints.menu_xmb_theme);
video_driver_monitor_reset();
}
static void xmb_navigation_clear(void *data, bool pending_push)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (!pending_push)
xmb_selection_pointer_changed(xmb, true);
}
static void xmb_navigation_pointer_changed(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
xmb_selection_pointer_changed(xmb, true);
}
static void xmb_navigation_set(void *data, bool scroll)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
xmb_selection_pointer_changed(xmb, scroll);
}
static void xmb_navigation_alphabet(void *data, size_t *unused)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
xmb_selection_pointer_changed(xmb, true);
}
static void xmb_list_insert(void *userdata,
file_list_t *list,
const char *path,
const char *fullpath,
const char *unused,
size_t list_size,
unsigned entry_type)
{
int current = 0;
int i = (int)list_size;
xmb_node_t *node = NULL;
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
struct menu_state *menu_st = menu_state_get_ptr();
size_t selection = menu_st->selection_ptr;
if (!xmb || !list)
return;
if (!(node = (xmb_node_t*)list->list[i].userdata))
{
if (!(node = xmb_alloc_node()))
return;
}
current = (int)selection;
if (fullpath && *fullpath)
{
if (node->fullpath)
menu_str_unref(node->fullpath);
node->fullpath = menu_str_ref(fullpath);
}
node->alpha = xmb->items_passive_alpha;
node->zoom = xmb->items_passive_zoom;
node->label_alpha = node->alpha;
node->y = xmb_item_y(xmb, i, current);
node->x = 0;
if (i == current)
{
node->alpha = xmb->items_active_alpha;
node->label_alpha = xmb->items_active_alpha;
node->zoom = xmb->items_active_alpha;
}
list->userdata_free = xmb_free_node_cb;
list->list[i].userdata = node;
}
static void xmb_list_clear(file_list_t *list)
{
uintptr_t tag = (uintptr_t)list;
gfx_animation_kill_by_tag(&tag);
xmb_free_list_nodes(list, false);
}
static void xmb_list_free(file_list_t *list, size_t a, size_t b)
{
xmb_list_clear(list);
}
static void xmb_list_cache(void *data, enum menu_list_type type,
unsigned action)
{
size_t stack_size, list_size;
xmb_handle_t *xmb = (xmb_handle_t*)data;
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
size_t selection = menu_st->selection_ptr;
unsigned horizontal_list_size = (xmb->show_playlist_tabs)
? (unsigned)xmb_list_get_size(xmb, MENU_LIST_HORIZONTAL)
: 0;
if (!xmb)
return;
/* Check whether to enable the horizontal animation. */
if (xmb->allow_horizontal_animation)
{
unsigned first = 0, last = 0;
unsigned height = xmb->last_height;
/* FIXME: this shouldn't be happening at all */
if (selection >= selection_buf->size)
selection = selection_buf->size ? selection_buf->size - 1 : 0;
xmb->selection_ptr_old = selection;
xmb_calculate_visible_range(xmb, height, selection_buf->size,
(unsigned)xmb->selection_ptr_old, &first, &last);
xmb->selection_ptr_old -= first;
last -= first;
first = 0;
}
else
xmb->selection_ptr_old = selection;
list_size = xmb->system_tab_end + horizontal_list_size;
switch (type)
{
case MENU_LIST_PLAIN:
break;
case MENU_LIST_HORIZONTAL:
xmb->categories_selection_ptr_old = xmb->categories_selection_ptr;
/* Remember last selection per tab */
xmb->tab_selection[xmb->categories_selection_ptr] = selection;
switch (action)
{
case MENU_ACTION_LEFT:
if (xmb->categories_selection_ptr == 0)
{
xmb->categories_selection_ptr = list_size;
xmb->categories_active_idx = (unsigned)(list_size - 1);
}
else
xmb->categories_selection_ptr--;
break;
default:
if (xmb->categories_selection_ptr == list_size)
{
xmb->categories_selection_ptr = 0;
xmb->categories_active_idx = 1;
}
else
xmb->categories_selection_ptr++;
break;
}
stack_size = menu_stack ? menu_stack->size : 0;
/* Relabelling needs an entry to relabel: with an empty stack,
* stack_size - 1 indexes SIZE_MAX and the free below releases
* whatever that read returns. Nothing else in this case
* depends on the relabel, so leave the switch. */
if (stack_size < 1)
break;
if (menu_stack->list[stack_size - 1].label)
free(menu_stack->list[stack_size - 1].label);
menu_stack->list[stack_size - 1].label = NULL;
switch (xmb_get_system_tab(xmb, (unsigned)xmb->categories_selection_ptr))
{
case XMB_SYSTEM_TAB_MAIN:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_MAIN_MENU_STR);
menu_stack->list[stack_size - 1].type = MENU_SETTINGS;
break;
case XMB_SYSTEM_TAB_SETTINGS:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_SETTINGS_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_SETTINGS_TAB;
break;
#ifdef HAVE_IMAGEVIEWER
case XMB_SYSTEM_TAB_IMAGES:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_IMAGES_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_IMAGES_TAB;
break;
#endif
case XMB_SYSTEM_TAB_MUSIC:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_MUSIC_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_MUSIC_TAB;
break;
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
case XMB_SYSTEM_TAB_VIDEO:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_VIDEO_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_VIDEO_TAB;
break;
#endif
case XMB_SYSTEM_TAB_HISTORY:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_HISTORY_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_HISTORY_TAB;
break;
case XMB_SYSTEM_TAB_FAVORITES:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_FAVORITES_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_FAVORITES_TAB;
break;
#ifdef HAVE_NETWORKING
case XMB_SYSTEM_TAB_NETPLAY:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_NETPLAY_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_NETPLAY_TAB;
break;
#endif
case XMB_SYSTEM_TAB_ADD:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_ADD_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_ADD_TAB;
break;
#if defined(HAVE_LIBRETRODB)
case XMB_SYSTEM_TAB_EXPLORE:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_EXPLORE_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_EXPLORE_TAB;
break;
#endif
case XMB_SYSTEM_TAB_CONTENTLESS_CORES:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_CONTENTLESS_CORES_TAB_STR);
menu_stack->list[stack_size - 1].type = MENU_CONTENTLESS_CORES_TAB;
break;
default:
menu_stack->list[stack_size - 1].label = strdup(MENU_ENUM_LABEL_HORIZONTAL_MENU_STR);
menu_stack->list[stack_size - 1].type = MENU_SETTING_HORIZONTAL_MENU;
break;
}
break;
default:
break;
}
}
static void xmb_context_destroy(void *data)
{
unsigned i;
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (!xmb)
return;
/* Signal the render path to stop using textures/fonts.
* Under threaded video, xmb_frame() may be mid-render on
* the video thread when this runs on the main thread. */
xmb->context_generation++;
/* Invalidate in-flight async icon loads before unloading */
xmb_icon_load_gen++;
xmb_ctx_icon_load_gen++;
for (i = 0; i < XMB_TEXTURE_LAST; i++)
video_driver_texture_unload(&xmb->textures.list[i]);
xmb_unload_thumbnail_textures(xmb);
xmb_unload_icon_thumbnail_textures(xmb);
/* The matching xmb_context_reset_internal will synchronously repopulate
* dynamic icons on a playlist. Clear any pending deferred flag so
* xmb_render does not fire between destroy and reset, which would walk
* the selection list with already-reset thumbnail state only to have
* it wiped and redone by the reset. */
xmb->pending_dynamic_icons_repopulate = false;
xmb_context_destroy_horizontal_list(xmb);
xmb_context_bg_destroy(xmb);
font_driver_free(xmb->font);
xmb->font = NULL;
font_driver_free(xmb->font2);
xmb->font2 = NULL;
menu_screensaver_context_destroy(xmb->screensaver);
}
static void xmb_toggle(void *userdata, bool menu_on)
{
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
struct menu_state *menu_st = menu_state_get_ptr();
if (!xmb)
return;
/* Reset */
xmb->alpha_list = 1.0f;
if (!menu_on)
{
xmb->alpha = 0;
return;
}
/* Have to reset this, otherwise savestate
* thumbnail won't update after selecting
* 'save state' option */
if (xmb->is_quick_menu)
{
gfx_thumbnail_reset(&xmb->thumbnails.savestate);
if (xmb->libretro_running)
xmb->fullscreen_thumbnails_available = false;
}
if (MENU_ENTRIES_NEEDS_REFRESH(menu_st))
menu_st->flags &= ~MENU_ST_FLAG_PREVENT_POPULATE;
else
menu_st->flags |= MENU_ST_FLAG_PREVENT_POPULATE;
/* Prevent initial jumping with startup page and CLI launch */
if (xmb->is_first_frame)
xmb->allow_horizontal_animation = false;
xmb_toggle_horizontal_list(xmb);
xmb->alpha = xmb->items_active_alpha;
}
static int xmb_deferred_push_content_actions(menu_displaylist_info_t *info)
{
settings_t *settings = config_get_ptr();
if (!menu_displaylist_ctl(DISPLAYLIST_HORIZONTAL_CONTENT_ACTIONS, info, settings))
return -1;
menu_displaylist_process(info);
menu_displaylist_info_free(info);
return 0;
}
static int xmb_list_bind_init_compare_label(menu_file_list_cbs_t *cbs)
{
if (cbs && cbs->enum_idx != MSG_UNKNOWN)
{
switch (cbs->enum_idx)
{
case MENU_ENUM_LABEL_CONTENT_ACTIONS:
cbs->action_deferred_push = xmb_deferred_push_content_actions;
break;
default:
return -1;
}
}
return 0;
}
static int xmb_list_bind_init(menu_file_list_cbs_t *cbs,
const char *path, const char *label, unsigned type, size_t idx)
{
if (xmb_list_bind_init_compare_label(cbs) == 0)
return 0;
return -1;
}
static int xmb_list_push(void *data, void *userdata,
menu_displaylist_info_t *info, unsigned type)
{
switch (type)
{
case DISPLAYLIST_MENU_SETTINGS_LIST:
{
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
xmb_update_dynamic_wallpaper(xmb, false);
break;
}
}
/* Use common lists for all drivers */
return -1;
}
static bool xmb_menu_init_list(void *data)
{
menu_displaylist_info_t info;
settings_t *settings = config_get_ptr();
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
file_list_t *menu_stack = MENU_LIST_GET(menu_list, 0);
file_list_t *selection_buf = MENU_LIST_GET_SELECTION(menu_list, 0);
menu_displaylist_info_init(&info);
info.label = strdup(MENU_ENUM_LABEL_MAIN_MENU_STR);
info.type_default = FILE_TYPE_PLAIN;
info.enum_idx = MENU_ENUM_LABEL_MAIN_MENU;
menu_entries_append(menu_stack, info.path,
info.label,
MENU_ENUM_LABEL_MAIN_MENU,
info.type, info.flags, 0, NULL);
info.list = selection_buf;
if (!menu_displaylist_ctl(DISPLAYLIST_MAIN_MENU, &info, settings))
goto error;
info.flags |= MD_FLAG_NEED_PUSH;
if (!menu_displaylist_process(&info))
goto error;
menu_displaylist_info_free(&info);
return true;
error:
menu_displaylist_info_free(&info);
return false;
}
static int xmb_pointer_down(void *userdata,
unsigned x, unsigned y,
unsigned ptr, menu_file_list_cbs_t *cbs,
menu_entry_t *entry, unsigned action)
{
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
if (!xmb)
return -1;
/* Initialize drag detection */
xmb->drag_mode = XMB_DRAG_DETECTING;
xmb->drag_start_x = (float)x;
xmb->drag_start_y = (float)y;
xmb->categories_drag_start_pos = xmb->categories_x_pos;
xmb->drag_start_selection = menu_state_get_ptr()->selection_ptr;
return 0;
}
static int xmb_pointer_up(void *userdata,
unsigned x, unsigned y, unsigned ptr,
enum menu_input_pointer_gesture gesture,
menu_file_list_cbs_t *cbs,
menu_entry_t *entry, unsigned action)
{
unsigned width;
unsigned height;
int16_t margin_top;
int16_t margin_left;
int16_t margin_right;
xmb_handle_t *xmb = (xmb_handle_t*)userdata;
struct menu_state *menu_st = menu_state_get_ptr();
size_t selection = menu_st->selection_ptr;
menu_list_t *menu_list = menu_st->entries.list;
unsigned end = (unsigned)MENU_LIST_GET_SELECTION(menu_list, 0)->size;
if (!xmb)
return -1;
/* If fullscreen thumbnail view is enabled,
* all input will disable it and otherwise
* be ignored */
if (xmb->show_fullscreen_thumbnails)
{
xmb_hide_fullscreen_thumbnails(xmb, true);
return 0;
}
/* Handle snap animation if we were dragging */
if (xmb->drag_mode == XMB_DRAG_HORIZONTAL || xmb->drag_mode == XMB_DRAG_VERTICAL)
{
/* Snap horizontal scrolling to final category position */
if (xmb->drag_mode == XMB_DRAG_HORIZONTAL && xmb->show_horizontal_list)
{
settings_t *settings = config_get_ptr();
bool horizontal_animation = settings->bools.menu_horizontal_animation;
float target_x = xmb->icon_spacing_horizontal * -(float)xmb->categories_selection_ptr;
/* Animate to exact category position */
if (horizontal_animation)
{
gfx_animation_ctx_entry_t anim_entry;
uintptr_t cat_tag = (uintptr_t)&xmb->categories_x_pos;
anim_entry.duration = XMB_DELAY;
anim_entry.target_value = target_x;
anim_entry.subject = &xmb->categories_x_pos;
anim_entry.easing_enum = EASING_OUT_QUAD;
anim_entry.tag = cat_tag;
anim_entry.cb = NULL;
/* See xmb_list_switch for the rationale — same unique-tag
* kill-before-push pattern so drag-release doesn't stack
* a tween on top of an in-flight tab-switch tween. */
gfx_animation_kill_by_tag(&cat_tag);
if (anim_entry.subject)
gfx_animation_push(&anim_entry);
}
else
{
uintptr_t cat_tag = (uintptr_t)&xmb->categories_x_pos;
gfx_animation_kill_by_tag(&cat_tag);
xmb->categories_x_pos = target_x;
}
}
xmb->drag_mode = XMB_DRAG_NONE;
return 0;
}
width = xmb->last_width;
height = xmb->last_height;
margin_top = (int16_t)xmb->margins_screen_top;
margin_left = (int16_t)xmb->margins_screen_left;
margin_right = (int16_t)((float)width - xmb->margins_screen_left);
switch (gesture)
{
case MENU_INPUT_GESTURE_TAP:
case MENU_INPUT_GESTURE_SHORT_PRESS:
/* Switch tabs */
if (xmb->depth == 1)
{
if ( (int)y >= margin_top - (xmb->icon_size + xmb->icon_spacing_vertical) / 2
&& (int)y <= margin_top + (xmb->icon_size + xmb->icon_spacing_vertical) / 2)
{
int current_tab_x = margin_left + xmb->icon_spacing_horizontal;
int tab_delta = floor(((int)x - current_tab_x + (xmb->icon_spacing_horizontal / 2)) / xmb->icon_spacing_horizontal);
if (tab_delta)
{
int i = 0;
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
menu_entry_get(&entry, 0, menu_st->selection_ptr, NULL, true);
/* Icon animations get stuck if they happen too fast,
* therefore allow it only on the last action */
xmb->allow_horizontal_animation = false;
/* Prevent dynamic wallpaper flashing during jump */
xmb->allow_dynamic_wallpaper = false;
if (tab_delta < 0)
{
for (i = tab_delta; i < 0; i++)
xmb_menu_entry_action(xmb,
&entry, menu_st->selection_ptr, MENU_ACTION_LEFT);
}
else
{
for (i = 0; i < tab_delta; i++)
xmb_menu_entry_action(xmb,
&entry, menu_st->selection_ptr, MENU_ACTION_RIGHT);
}
xmb->allow_horizontal_animation = true;
xmb->allow_dynamic_wallpaper = true;
}
return MENU_ACTION_NOOP;
}
}
/* - A touch in the left margin:
* > ...triggers a 'cancel' action beneath the top margin
* > ...triggers a 'search' action above the top margin
* - A touch in the right margin triggers a 'select' action
* for the current item
* - Between the left/right margins input is handled normally */
if ((int)x < margin_left)
{
if ((int)y >= margin_top)
return xmb_menu_entry_action(xmb, entry, selection, MENU_ACTION_CANCEL);
return menu_input_dialog_start_search() ? 0 : -1;
}
else if ((int)x > margin_right)
return xmb_menu_entry_action(xmb, entry, selection, MENU_ACTION_SELECT);
else if (ptr <= (end - 1))
{
/* Perform 'select' on the pointed item */
menu_st->selection_ptr = ptr;
xmb_navigation_set(xmb, false);
return xmb_menu_entry_action(xmb, entry, ptr, MENU_ACTION_SELECT);
}
break;
case MENU_INPUT_GESTURE_LONG_PRESS:
/* 'Reset to default' action */
if ((ptr <= end - 1) && (ptr == selection))
return xmb_menu_entry_action(xmb, entry, selection, MENU_ACTION_START);
break;
case MENU_INPUT_GESTURE_SWIPE_LEFT:
/* Navigate left
* Note: At the top level, navigating left
* means switching to the 'next' horizontal list,
* which is actually a movement to the *right* */
if ((int)y > margin_top)
xmb_menu_entry_action(xmb, entry, selection,
(xmb->depth == 1) ? MENU_ACTION_RIGHT : MENU_ACTION_LEFT);
break;
case MENU_INPUT_GESTURE_SWIPE_RIGHT:
/* Navigate right
* Note: At the top level, navigating right
* means switching to the 'previous' horizontal list,
* which is actually a movement to the *left* */
if ((int)y > margin_top)
xmb_menu_entry_action(xmb, entry, selection,
(xmb->depth == 1) ? MENU_ACTION_LEFT : MENU_ACTION_RIGHT);
break;
case MENU_INPUT_GESTURE_SWIPE_UP:
/* Swipe up in left margin: ascend alphabet */
if ((int)x < margin_left)
xmb_menu_entry_action(xmb, entry, selection, MENU_ACTION_SCROLL_DOWN);
else if ((int)x < margin_right)
{
/* Swipe up between left and right margins:
* move selection pointer down by 1 'page' */
unsigned first = 0;
unsigned last = end;
if (height)
xmb_calculate_visible_range(xmb, height, end,
(unsigned)selection, &first, &last);
if (last < end)
{
menu_st->selection_ptr = (size_t)last;
xmb_navigation_set(xmb, true);
}
else
{
size_t menu_list_size = menu_st->entries.list
? MENU_LIST_GET_SELECTION(menu_st->entries.list, 0)->size : 0;
size_t new_selection = menu_list_size - 1;
menu_st->selection_ptr = new_selection;
if (menu_st->driver_ctx->navigation_set_last)
menu_st->driver_ctx->navigation_set_last(menu_st->userdata);
}
}
break;
case MENU_INPUT_GESTURE_SWIPE_DOWN:
/* Swipe down in left margin: descend alphabet */
if ((int)x < margin_left)
xmb_menu_entry_action(xmb,
entry, selection, MENU_ACTION_SCROLL_UP);
else if ((int)x < margin_right)
{
/* Swipe down between left and right margins:
* move selection pointer up by 1 'page' */
unsigned bottom_idx = (unsigned)selection + 1;
size_t new_idx = 0;
size_t step = 0;
/* Determine index of entry at bottom of screen
* Note: cannot use xmb_calculate_visible_range()
* here because there may not be sufficient entries
* to reach the bottom of the screen - i.e. we just
* want an index offset to subtract from the current
* selection... */
for (;;)
{
float top = xmb_item_y(xmb, bottom_idx, selection) + xmb->margins_screen_top;
if (top > height)
{
/* Since this checks the top position, the
* final index is always 1 greater than it
* should be... */
bottom_idx--;
break;
}
bottom_idx++;
}
if (bottom_idx >= selection)
step = bottom_idx - selection;
if (selection > step)
new_idx = selection - step;
if (new_idx > 0)
{
menu_st->selection_ptr = new_idx;
xmb_navigation_set(xmb, true);
}
else
{
bool pending_push = false;
menu_driver_ctl(MENU_NAVIGATION_CTL_CLEAR, &pending_push);
}
}
break;
default:
/* Ignore input */
break;
}
return 0;
}
menu_ctx_driver_t menu_ctx_xmb = {
NULL,
xmb_messagebox,
xmb_render,
xmb_frame,
xmb_init,
xmb_free,
xmb_context_reset,
xmb_context_destroy,
xmb_populate_entries,
xmb_toggle,
xmb_navigation_clear,
NULL, /*xmb_navigation_pointer_changed,*/ /* Note:
navigation_set() is called each time
navigation_increment/decrement() */
NULL, /*xmb_navigation_pointer_changed,*/ /* is called, so linking these just duplicates work... */
xmb_navigation_set,
xmb_navigation_pointer_changed,
xmb_navigation_alphabet,
xmb_navigation_alphabet,
xmb_menu_init_list,
xmb_list_insert,
NULL,
xmb_list_free,
xmb_list_clear,
xmb_list_cache,
xmb_list_push,
xmb_list_get_selection,
xmb_list_get_size,
xmb_list_get_entry,
NULL,
xmb_list_bind_init,
xmb_load_image,
"xmb",
xmb_environ,
NULL,
xmb_update_thumbnail_image,
xmb_refresh_thumbnail_image,
xmb_set_thumbnail_content,
gfx_display_osk_ptr_at_pos,
xmb_osk_pointer_over_textbox,
xmb_update_savestate_thumbnail_path,
xmb_update_savestate_thumbnail_image,
xmb_pointer_down,
xmb_pointer_up,
xmb_menu_entry_action
};