Files
RetroArch/menu/drivers/xmb.c
T
Davey HughesandLibretroAdmin 08e5b72a88 Scan from the command line: exit status, Manual Scan options, playlist refresh (#19977)
* file_path: stop at the end of the path after '~'

fill_pathname_expand_special() skipped two characters after a leading
'~' or ':', so a bare "~" read past its NUL and "~x" lost the x. It
now skips the prefix and a slash after it, if there is one.

* scan: hand the callback user data and what the scan did

task_push_dbscan() and task_push_manual_content_scan() take user data
for their callback, and the callback's task_data is now a
task_dbscan_result_t: how many entries the scan added, how many its
playlists already held, and how many files matched nothing. A file is
counted by what became of it, so an archive that is expanded is not
counted as unmatched, and a loose scan's unmatched files count as
added. Callers pass NULL for now.

* scan: say in the result when a scan failed

A scan that ends before it runs (bad setup, no valid content, a DAT
that will not load) or ends with a task error now sets the result's
failed flag. A database folder that is missing or unset is treated as
an empty one, so it reports the same "no database" error instead of
finishing as if it had scanned.

* scan: count the files a scan could not read

A file whose scan fails (an unreadable file, a failed lookup) was in
no count. The result now carries an errors count for them. They do
not fail the scan.

* scan: print --scan errors on stderr and totals on stdout

A command-line scan now ends with a totals line on stdout, or with the
reason it failed on stderr. A file it could not read is listed on
stderr. The ++/==/?? marks are coloured only when their stream is a
terminal, so pipes and redirected output get plain text; on Windows
redirected output keeps its marks instead of losing them to
WriteConsole.

* scan: the manual scan finder skips a finished task

A finished scan stays in the task queue while its callback runs, and
the finder that refuses a second scan for the same playlist still
matched it, so a scan pushed from that callback was refused.

* retroarch: run every --scan, then exit with what the scans did

Each --scan is collected while the command line is parsed and run in
turn once it is done, so later options and further --scan flags are
no longer dropped. A path is expanded and made absolute first; one
that does not exist, or a scan that cannot start, is reported on
stderr and the next path still runs. --scan with --cloudsync or
--command is refused.

Without --menu the exit status is 1 if any scan failed, else 0 if any
added an entry, else 2, and a content path given with the scans is
ignored, with a line on stderr. With --menu each finished scan starts
the next, as a second scan is refused while one runs.

* manual_content_scan: clear the database name a refresh did not save

A playlist with no saved database name left the name buffer
uninitialised, and the refresh compared it against every installed
database, so whatever was on the stack could pick a database the
playlist never named.

* scan: Skip Database References names the scan's one playlist

The option was meant to put the playlist's name in each entry's
db_name, even for a database match (#18641), for users who build
their own thumbnail sets. When the scan picks the playlists, db_name
also picks the playlist an entry goes to, so with the option on every
match went to one playlist named after the content folder.

It now applies only when the scan writes one playlist, and gives
that playlist's name, a loose scan's unmatched entries included. With
the target on <Automatic>, or Fully Automatic with the option left on
from a Custom scan, matches go to their databases' playlists as with
it off; the entries of a custom playlist look their thumbnails up
under its name, not the folder's.

* scan: name the scanned folder in the start line

The "Scanning content" line, in the log and in --scan's output, has
named the database folder since the manual scan and the database scan
were merged (2fdf736d51); before, it named the path being scanned. It
names the content folder again.

* manual_content_scan: reset, scan overrides and their apply

The command line's scan options need each scan to start from the
defaults and to lay their values over them, or over a playlist's saved
settings. Add a reset, a record of those options and a function that
applies it through the existing setters: a core named by path, file
name or display name, a database, a playlist, the database check or a
DAT, extensions and the switches, stopping with a reason at the first
that does not resolve.

A core path is normalised before it is compared. A core's file name
matches in the case given first, then ignoring case, on every
platform; a display name matches exactly. Given another database, a
playlist named after its database keeps that name as a custom one, so
a refresh still writes the same playlist.

The core setter's copy step becomes its own function, so a core found
by path or file name can be set. Nothing calls the new functions yet.

* scan: run --scan items through one chain

The --scan paths ran through a loop in retroarch.c, and with --menu
through a second chain of their own. The scan options and playlist
refreshes to come need one runner for every item, from the command
line and from the menu.

task_push_dbscan_chain() runs a list of items one task at a time: a
prep task lists the databases, then each step is pushed from the last
task's callback on the main thread, so nothing waits and nothing
locks. An item that fails is reported and the run goes on; a
cancelled task ends it. retroarch.c builds the items in the first
argument pass and hands them to the chain; without --menu it waits
for the chain's end and exits as before.

Running out of memory prints "out of memory" on stderr: for an item's
path the item fails, and for the chain itself nothing runs, which
without --menu exits 1.

The chain also prints a scan's override errors, and the warnings for
an option that needs one playlist or does nothing for a single file,
which the scan options of the next commit can trigger.

* retroarch: Manual Scan options for --scan

--scan-playlist, --scan-database, --scan-match, --scan-dat, --scan-core,
--scan-exts and the five switches set the Manual Scan settings of the
--scan before them; a --scan without any stays Fully Automatic.

Values are checked in the first pass, so an option before any --scan,
an empty or bad value, or one too long for its setting ends the run
with its reason on stderr and status 1, before the config loads and
before anything is scanned. Cores, databases and DAT files are
resolved when their scan runs, and one that does not resolve fails
only that scan.

* scan: refresh playlists in the chain

A refresh item rescans one playlist with its saved scan settings, as
the menu's Refresh Playlist does, with the item's scan options laid
over them and written back by the scan. A refresh-all item does the
same for every playlist in the playlist folder that has saved
settings, in the order Manage Playlists lists them, histories and
favorites left out.

A refresh item names its playlist by name or by a path in the playlist
folder. A bare "name.lpl" is the playlist folder's file, and an
absolute path is normalised before it is checked against the folder.
The name is looked up in the folder's listing on every platform, the
exact name first, then ignoring case, since macOS and FAT/exFAT cards
ignore case; the refresh then takes the file's own name, so a
database's playlist stays one. Two or more playlists that match only
in other cases fail the item.

The playlist folder walk and each playlist load run in prep tasks: on
the worker with a threaded queue, under the shared I/O window without
one, each pushed from the previous step's callback. A playlist whose
saved content folder is gone is skipped rather than failed. A listing
that runs out of memory fails the refresh-all; a playlist folder that
is missing or cannot be read lists nothing, and the refresh-all ends
with "no playlists with saved scan settings". A refresh's start line
names its content folder; its totals and failure lines name the
playlist.

* retroarch: --scan-refresh and --scan-refresh-all

--scan-refresh=NAME|PATH rescans a playlist with its saved scan
settings, as the menu's Refresh Playlist does, and --scan-refresh-all
rescans every playlist that has them. Both are items of the same run
as --scan: the scan options after one are laid over the saved
settings and written back, and the exit status follows the same rule.

--scan-playlist after a refresh is refused, so a refresh always
updates its own playlist, and an empty --scan-refresh is refused as
the other empty values are. Like --scan, the refresh options cannot be
combined with --cloudsync or --command; the error names whichever of
the three comes first on the command line.

* menu: Refresh All Playlists

Manage Playlists gets Refresh All Playlists as its first entry when it
lists any playlist, so an empty list still says there are no entries.
It rescans every playlist that has saved scan settings, with those
settings, through the same chain as --scan-refresh-all, and ends with
one notification saying how many playlists were refreshed, skipped
(content folder gone) and failed.

While a content scan runs it is refused with a new message; a refused
scan had none. Other languages come through Crowdin.

While a scan chain runs (Refresh All, or a command-line scan with
--menu), Start Scan, a playlist's Refresh, Scan Directory and Scan
File are refused with the same message, as they would run with the
settings the chain last loaded. task_push_dbscan() returns false then
too, which covers the companion and the iOS caller.

* docs: --scan, refresh and the Manual Scan options in the man page

--help lists the options but not what a script needs: the order the
scans run in, the exit status and what the options set. The man page
now has them, next to --cloudsync, and CHANGES.md has the entry.

* scan chain: writes from workers reach the playlist cache by note

A playlist written by a task - the chain's prep parse, a scan, a
playlist-manager task, whatever thread it runs on - leaves a note on a
lock-free stack instead of reading the cached playlist; the main
thread applies the notes when it next decides whether the cache may
stand in, so no worker reads playlist_cached while the main thread
frees or installs it. Without threads or pointer atomics a write
applies at once.

The chain's prep tasks set their handlers by name, and
thread_read_audit.py fails on a handler set from a
retro_task_handler_t parameter, which would hide every handler passed
in. The chain puts the Manual Scan settings it found back when it
ends, and is no longer running when its done callback runs, so that
callback may start another. task_push_dbscan() says why it refuses,
so the menu and the companions show the same reason, and the
companions' status line takes it from companion_core. --scan and
--scan-refresh set the verbosity override, so a config with logging
off cannot detach the console the totals go to, and refuse a path too
long for the chain's buffers. The Refresh All notification passes the
length written, and fill_pathname_expand_special() stops at the end of
a buffer shorter than the home or application directory.

Lanes: playlist_parity_test writes the cached file from a worker,
once at the same size and then while the main thread reuses and frees
the cache; database_scan_test checks the refusal message, the restored
settings and a done callback that starts the next chain;
fill_pathname_test expands into a buffer shorter than the home
directory.

* playlist: header-only parse for the scan chain's prep

The prep task needs a playlist's saved scan settings and nothing else,
yet parsed every entry and, through autofix, could write the file back
from a worker. playlist_parse_begin_header() stops at "items", rebases
the scan record in memory when the base content directory moved, and
marks the playlist so playlist_write_file() refuses it.

Lane: playlist_parity_test header_only (entries skipped, scan dir
rebased, file bytes untouched, old format).

* scan chain: ask the file system nothing on the main thread

The chain's steps ran on the main thread between its tasks and stat'd
and realpath'd there: a --scan path's resolve and stat, a
--scan-refresh path's two realpaths, the content folder's stat, the
saved and override DATs' stat and size, an override core path's
realpath.  On a network share each of those can stall a frame.

- manual_content_scan_fs_read() reads them on any thread, touching
  nothing but its arguments; the _fs variants of the content-folder,
  playlist and override setters take its answers instead of asking.
  The plain setters pass NULL and behave as before, so the menu is
  unchanged.
- A --scan path gets its own prep task (dbscan_path_handler); a
  --scan-refresh resolves its path inside the load task, and the load
  reads the folder, the saved DAT and the overrides' paths once the
  header is parsed.  The worker gets copies of the item's strings.
- The prep's state is freed in one place.

Lane: database_scan_test "fs off main": six items that each stop
before their scan, on a threaded queue, with path_stat, path_is_valid,
path_is_directory, path_get_size and path_resolve_realpath wrapped:
no call on the main thread.

Not moved: the scan task's own push still stats the content folder
and the DAT (manual_content_scan_get_task_config), shared with the
menu.

* samples: the scan chain's lanes hold on Windows and without realpath

From a native Windows run of both tests:

- database_scan_test "fs off main" refreshed f_gone.lpl through
  "pl/../pl/", which only a realpath folds; the sample builds without
  RARCH_INTERNAL, where path_resolve_realpath() folds nothing, so the
  item was rightly not in the playlist folder.  It names the playlist
  by its plain path now, which the check still realpaths on the
  worker.
- "another case, a database's playlist" read the system name type
  after the chain ended, and the chain puts the Manual Scan settings
  back then.  The item callback records each scan's type instead.
- playlist_parity_test's header lane expected "/moved/snes"; autofix
  converts the suffix to the local form, "/moved\snes" on Windows.
- The worker lane's stress read required a core name of every read.
  Windows replaces the file with MoveFileExW, and an open that meets
  the replace fails: that read is an empty playlist, which has none.

---------

Co-authored-by: LibretroAdmin <[email protected]>
2026-11-04 19:34:05 +01:00

10816 lines
403 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 "../../gfx/gfx_surface.h"
#include "../../frontend/frontend_driver.h"
#include "../menu_driver.h"
#include "../menu_media_controls.h"
#include "../menu_media_browser.h"
#include "../menu_str.h"
#include "../menu_entries.h"
#include "../menu_screensaver.h"
#include "../../gfx/gfx_animation.h"
#include "../../gfx/gfx_thumbnail.h"
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
#include "../../gfx/common/win32_common.h"
#endif
#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)
/* How long (us) xmb_render() keeps retrying xmb_set_title()
* while an async-loaded icon handle is still unresolved */
#define XMB_ICON_RETRY_TIMEOUT 1000000
#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
enum
{
XMB_TEXTURE_MAIN_MENU = 0,
XMB_TEXTURE_SETTINGS,
XMB_TEXTURE_HISTORY,
XMB_TEXTURE_FAVORITES,
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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_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
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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;
gfx_surface_t *icon;
gfx_surface_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
{
/* The ribbon's grid, drawn by the ribbon programs */
gfx_display_mesh_t *ribbon_mesh;
/* 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 */
retro_time_t draw_entry_hold_until;
/* Deadline (us) until which xmb_render() retries
* xmb_set_title() after a context reset left an
* async-loaded sidebar / db-node icon unresolved; zero
* when no retry is pending */
retro_time_t current_menu_icon_retry_until;
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;
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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;
menu_media_controls_t media;
/* 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
{
gfx_surface_t *bg;
gfx_surface_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_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;
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];
char title_name[NAME_MAX_LENGTH];
char title_name_alt[NAME_MAX_LENGTH];
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;
/* 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_dims. */
/* The video size this was last laid out for, one word,
* VIDEO_SCALE_PACK's layout. */
unsigned last_dims;
/* 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;
/* What a pointer press needs to look at an entry with. Here rather
* than on the frame of xmb_render(), which runs every frame: a
* menu_entry_t is 3872 bytes, and the frame measured 4312 where this
* tree allows four thousand. One is looked at a time. */
menu_entry_t render_entry;
} 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, int input_turbo_bind, unsigned input_turbo_button);
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 = NULL;
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_node_free_icons(xmb_node_t *node)
{
gfx_surface_free(node->icon);
gfx_surface_free(node->content_icon);
node->icon = NULL;
node->content_icon = NULL;
}
static void xmb_free_node(xmb_node_t *node)
{
if (!node)
return;
xmb_node_free_icons(node);
/* 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_dims,
bool shadows_enable,
int icon_size_x,
int icon_size_y,
uintptr_t texture,
float x,
float y,
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;
unsigned width = VIDEO_SCALE_W(video_dims);
unsigned height = VIDEO_SCALE_H(video_dims);
/* 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;
#if defined(VITA) || defined(WIIU) || defined(__PS3__)
/* These backends take the scale in the size rather than through
* the matrix. */
draw.dims = VIDEO_SCALE_PACK(
(unsigned)(icon_size_x * scale_factor),
(unsigned)(icon_size_y * scale_factor));
#else
draw.dims = VIDEO_SCALE_PACK(icon_size_x, icon_size_y);
#endif
draw.rotation = rotation;
draw.scale_factor = scale_factor;
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.pos = VIDEO_POS_PACK(VIDEO_PX(x + shadow_offset),
VIDEO_PX(height - y - shadow_offset));
#if defined(VITA) || defined(WIIU) || defined(__PS3__)
if (scale_factor < 1)
draw.pos = VIDEO_POS_PACK(
VIDEO_POS_X(draw.pos)
+ (icon_size_x - (int)VIDEO_SCALE_W(draw.dims)) / 2,
VIDEO_POS_Y(draw.pos)
+ (icon_size_y - (int)VIDEO_SCALE_H(draw.dims)) / 2);
#endif
if (VIDEO_SCALE_H(draw.dims) > 0 && VIDEO_SCALE_W(draw.dims) > 0)
gfx_display_draw(dispctx, &draw, userdata,
video_dims);
}
coords.color = (const float*)color;
draw.pos = VIDEO_POS_PACK(VIDEO_PX(x), VIDEO_PX(height - y));
#if defined(VITA) || defined(WIIU) || defined(__PS3__)
if (scale_factor < 1)
draw.pos = VIDEO_POS_PACK(
VIDEO_POS_X(draw.pos)
+ (icon_size_x - (int)VIDEO_SCALE_W(draw.dims)) / 2,
VIDEO_POS_Y(draw.pos)
+ (icon_size_y - (int)VIDEO_SCALE_H(draw.dims)) / 2);
#endif
if (VIDEO_SCALE_H(draw.dims) > 0 && VIDEO_SCALE_W(draw.dims) > 0)
gfx_display_draw(dispctx, &draw, userdata,
video_dims);
}
static void xmb_draw_text(
bool shadows_enable,
xmb_handle_t *xmb,
const video_frame_info_t *video_info,
const char *str,
float x, float y,
float scale_factor,
float alpha,
enum text_alignment text_align,
unsigned video_dims,
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(
video_info->menu.font_color_red,
video_info->menu.font_color_green,
video_info->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] = video_info->menu.font_color_red * (1.0f / 255.0f);
color_hp[1] = video_info->menu.font_color_green * (1.0f / 255.0f);
color_hp[2] = video_info->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,
video_dims, 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,
const video_frame_info_t *video_info,
gfx_display_t *p_disp,
gfx_display_ctx_driver_t *dispctx,
unsigned video_dims,
xmb_handle_t *xmb,
const char *message,
bool draw_caret,
math_matrix_4x4 *mymat)
{
unsigned video_width = VIDEO_SCALE_W(video_dims);
unsigned video_height = VIDEO_SCALE_H(video_dims);
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;
bool draw_selection = draw_caret && input_driver_keyboard_line_selected();
unsigned selection_first_line = 1;
float selection_color[16] = {
0.20f, 0.40f, 0.80f, 0.35f,
0.20f, 0.40f, 0.80f, 0.35f,
0.20f, 0.40f, 0.80f, 0.35f,
0.20f, 0.40f, 0.80f, 0.35f,
};
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)
{
size_t line_cursor = 0;
const char *line_text = input_driver_keyboard_line_view(&line_cursor);
const char *input = strchr(message, '\n');
draw_caret = false;
if (input && line_text
&& (draw_selection || ((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_cursor;
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);
if (draw_selection)
{
cursor_src[len] = '\0';
(xmb->word_wrap)(cursor_message, sizeof(cursor_message),
cursor_src, len, usable_width / (xmb->font_size * 0.85f),
xmb->wideglyph_width, 0);
selection_first_line = string_count_occurrences_single_character(
cursor_message, '\n');
if (!selection_first_line)
selection_first_line = 1;
}
memcpy(cursor_src + len, line_text, 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 = !draw_selection;
}
}
}
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_driver_keyboard_textbox_focus() && line_count > 1)
{
int cursor_offset = (xmb->margins_dialog + (xmb->margins_slice * 2)) / 3;
gfx_display_blend_begin(dispctx, userdata);
gfx_display_draw_texture_slice(
p_disp,
userdata,
video_dims,
slice_x - cursor_offset,
slice_y - cursor_offset,
VIDEO_SCALE_PACK(256, 256),
VIDEO_SCALE_PACK(slice_w + (cursor_offset * 2),
slice_h + (cursor_offset * 2)),
video_dims,
NULL,
xmb->margins_slice,
xmb->last_scale_factor,
GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_KEY_HOVER]),
mymat);
gfx_display_blend_end(dispctx, userdata);
}
if (!xmb->assets_missing)
{
gfx_display_blend_begin(dispctx, userdata);
gfx_display_draw_texture_slice(
p_disp,
userdata,
video_dims,
slice_x,
slice_y,
VIDEO_SCALE_PACK(256, 256),
VIDEO_SCALE_PACK(slice_w, slice_h),
video_dims,
NULL,
xmb->margins_slice,
xmb->last_scale_factor,
GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_DIALOG_SLICE]),
mymat);
gfx_display_blend_end(dispctx, 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_dims,
slice_x,
slice_y,
VIDEO_SCALE_PACK(slice_w, slice_h),
video_dims,
frame_color,
NULL);
}
for (i = 0; i < line_count; i++)
{
const char *msg = lines[i];
if (draw_selection && i >= selection_first_line && msg && *msg)
{
gfx_display_blend_begin(dispctx, userdata);
gfx_display_draw_quad(p_disp, userdata, video_dims,
x - (longest_width / 2.0), y + (i * line_height),
VIDEO_SCALE_PACK(font_driver_get_message_width(
xmb->font, msg, strlen(msg), 1.0f), line_height),
video_dims, selection_color, NULL);
gfx_display_blend_end(dispctx, userdata);
}
if (msg)
gfx_display_draw_text(xmb->font, msg,
x - (longest_width / 2.0),
y + ((i + 0.85) * line_height),
video_dims, 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_dims,
x - (longest_width / 2.0) + cursor_x,
y + ((cursor_line + 0.85) * line_height) - xmb->font->size,
VIDEO_SCALE_PACK(2, xmb->font->size),
video_dims,
caret_color,
NULL);
}
if (input_dialog_display_kb)
{
struct menu_state *menu_st = menu_state_get_ptr();
gfx_display_draw_keyboard(
p_disp,
userdata,
video_dims,
GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_KEY_HOVER]),
xmb->font,
menu_st->osk_grid,
input_driver_keyboard_textbox_focus() ? 44 : menu_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,
};
bool input_menu_swap_ok_cancel_buttons = video_info->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 ( VIDEO_POS_X(xmb->pointer.pos) >= cursor_x
&& VIDEO_POS_X(xmb->pointer.pos) <= cursor_x + cursor_w
&& VIDEO_POS_Y(xmb->pointer.pos) >= cursor_y
&& VIDEO_POS_Y(xmb->pointer.pos) <= cursor_y + cursor_h)
{
menu_st->dialog_st.confirm_hover_back = true;
gfx_display_draw_quad(
p_disp,
userdata,
video_dims,
cursor_x,
cursor_y,
VIDEO_SCALE_PACK(cursor_w, cursor_h),
video_dims,
frame_color,
NULL);
}
else
menu_st->dialog_st.confirm_hover_back = false;
gfx_display_blend_begin(dispctx, userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_dims,
true,
icon_size,
icon_size,
input_menu_swap_ok_cancel_buttons
? GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R])
: GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D]),
icon_x,
icon_y,
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_dims,
0x444444ff,
TEXT_ALIGN_LEFT,
1.0f,
false,
1.0f,
false);
gfx_display_blend_end(dispctx, 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 ( VIDEO_POS_X(xmb->pointer.pos) >= cursor_x
&& VIDEO_POS_X(xmb->pointer.pos) <= cursor_x + cursor_w
&& VIDEO_POS_Y(xmb->pointer.pos) >= cursor_y
&& VIDEO_POS_Y(xmb->pointer.pos) <= cursor_y + cursor_h)
{
menu_st->dialog_st.confirm_hover_ok = true;
gfx_display_draw_quad(
p_disp,
userdata,
video_dims,
cursor_x,
cursor_y,
VIDEO_SCALE_PACK(cursor_w, cursor_h),
video_dims,
frame_color,
NULL);
}
else
menu_st->dialog_st.confirm_hover_ok = false;
gfx_display_blend_begin(dispctx, userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_dims,
true,
icon_size,
icon_size,
input_menu_swap_ok_cancel_buttons
? GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D])
: GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R]),
icon_x,
icon_y,
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_dims,
0x444444ff,
TEXT_ALIGN_LEFT,
1.0f,
false,
1.0f,
false);
gfx_display_blend_end(dispctx, userdata);
}
}
static bool xmb_osk_pointer_over_textbox(
void *data,
int x,
int y,
unsigned dims)
{
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));
/* No file: no wallpaper, as a NULL bg_file_path is */
if (!path_is_valid(path))
path[0] = '\0';
if ( !string_is_equal(path, xmb->bg_file_path
? xmb->bg_file_path : "")
|| reset)
{
if (path[0])
{
if (reset)
{
xmb_context_bg_destroy(xmb);
if (!gfx_surface_submit_file(gfx_surface_still(&xmb->textures.bg,
gfx_display_texture_filter()),
path, gfx_surface_wants_rgba()))
task_push_image_load(path,
gfx_surface_wants_rgba(), 0,
0,
menu_display_handle_wallpaper_upload, NULL);
}
else
task_push_image_load(path,
gfx_surface_wants_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)
{
/* Off the frame: two paths and a menu_entry_t came to 8008 bytes
* where this tree allows four thousand. This runs when the selection
* moves, not every frame, so one allocation costs less than the
* frame did. */
struct savestate_thumb_scratch
{
menu_entry_t entry;
char old_path[PATH_MAX_LENGTH];
char path[PATH_MAX_LENGTH];
} *scratch;
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;
if (!(scratch = (struct savestate_thumb_scratch*)malloc(sizeof(*scratch))))
return;
old_path = scratch->old_path;
if (!xmb)
{ free(scratch); return; }
savestate_thumbnail = settings->bools.savestate_thumbnail_enable;
strlcpy(old_path, xmb->savestate_thumbnail_file_path, PATH_MAX_LENGTH);
if (xmb->skip_thumbnail_reset)
{ free(scratch); 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))
{ free(scratch); return; }
xmb->fullscreen_thumbnails_available = false;
if (savestate_thumbnail)
{
menu_entry_t *entry = &scratch->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 = scratch->path;
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, PATH_MAX_LENGTH,
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);
}
}
}
free(scratch);
}
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 || menu_media_controls_settings_active(&xmb->media))
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 || menu_media_controls_settings_active(&xmb->media))
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);
/* selection_ptr can exceed the list size when the list is
* rebuilt before the navigation pointer is re-clamped
* (cf. #18797). */
xmb_node_t *node = (selection < selection_buf->size)
? (xmb_node_t*)selection_buf->list[selection].userdata
: NULL;
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[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)
strlcpy(path_data->content_label,
content_label, sizeof(path_data->content_label));
else
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 = VIDEO_SCALE_H(xmb->last_dims);
/* 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 */
xmb->entry_index_str[0] = '\0';
if ( config_get_ptr()->bools.playlist_show_entry_idx
&& (xmb->is_playlist || xmb->is_explore_list))
{
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 = true;
if (xmb->is_explore_list)
{
if (entry_idx_selection > entry_idx_offset && entry_idx_offset)
entry_idx_selection -= entry_idx_offset;
else
show_entry_idx = false;
if (list_size >= entry_idx_offset)
list_size -= entry_idx_offset;
}
if (show_entry_idx)
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_media_controls_settings_active(&xmb->media))
{
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, (unsigned)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 = VIDEO_SCALE_H(xmb->last_dims);
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 = VIDEO_SCALE_H(xmb->last_dims);
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;
}
/* Missing-icon retry: a fresh miss opens a window of
* XMB_ICON_RETRY_TIMEOUT us against the menu's frame-sampled
* clock; a miss inside the window keeps it, and a miss after
* it has passed gives up, so a permanently-missing asset
* cannot cause a perpetual retry loop */
static retro_time_t xmb_icon_retry_next(retro_time_t prev)
{
retro_time_t now = menu_driver_get_current_time();
if (!prev)
return now + XMB_ICON_RETRY_TIMEOUT;
return (now < prev) ? prev : 0;
}
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 = GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_QUICKMENU]);
bool search = true;
/* Preserve any in-flight retry deadline. Cleared
* unconditionally first — a fallback branch below sets
* the next value via xmb_icon_retry_next() */
retro_time_t prev_retry = xmb->current_menu_icon_retry_until;
xmb->current_menu_icon_retry_until = 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_KEYBOARD_INDEX
&& enum_idx <= MENU_ENUM_LABEL_INPUT_KEYBOARD_INDEX_LAST)
enum_idx = MENU_ENUM_LABEL_INPUT_KEYBOARD_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 = GFX_SURFACE_HANDLE(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 && GFX_SURFACE_HANDLE(sidebar_node->icon))
texture = GFX_SURFACE_HANDLE(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_until = xmb_icon_retry_next(prev_retry);
}
/* 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)
? GFX_SURFACE_HANDLE(db_node->icon) : GFX_SURFACE_HANDLE(db_node->content_icon);
if (db_icon)
texture = db_icon;
else
xmb->current_menu_icon_retry_until = xmb_icon_retry_next(prev_retry);
}
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_until = xmb_icon_retry_next(prev_retry);
else if (string_ends_with(pl_config->path, FILE_PATH_CONTENT_IMAGE_HISTORY))
texture = GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_IMAGE]);
else if (string_ends_with(pl_config->path, FILE_PATH_CONTENT_MUSIC_HISTORY))
texture = GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MUSIC]);
else if (string_ends_with(pl_config->path, FILE_PATH_CONTENT_VIDEO_HISTORY))
texture = GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MOVIE]);
else if (xmb->categories_selection_ptr < xmb->system_tab_end)
texture = GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FILE]);
}
goto end;
}
#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
}
{
/* Main-thread path (menu navigation): the live settings are
* this context's source, exactly as the snapshot is the
* frame path's. */
settings_t *settings = config_get_ptr();
texture = xmb_icon_get_id(xmb, NULL, NULL, enum_idx, path,
label, type, 0, false,
settings->ints.input_turbo_bind,
settings->uints.input_turbo_button);
}
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;
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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)
{
default: /* a value out of range in the config file */
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)
{
/* one slot per tab, for as many tabs as the array holds */
size_t tab_selection =
(xmb->categories_selection_ptr < ARRAY_SIZE(xmb->tab_selection))
? xmb->tab_selection[xmb->categories_selection_ptr]
: 0;
if (tab_selection)
{
struct menu_state *menu_st = menu_state_get_ptr();
menu_list_t *menu_list = menu_st->entries.list;
/* a selection remembered from a longer list lands on its last
* entry */
if (menu_list)
tab_selection = menu_entries_restorable_selection(
MENU_LIST_GET_SELECTION(menu_list, 0), tab_selection);
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")))
{
xmb_node_free_icons(node);
}
}
}
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;
}
}
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 = gfx_surface_wants_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);
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_surface_submit_path(gfx_surface_still(&node->icon,
gfx_display_texture_filter()),
texturepath, supports_rgba);
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_surface_submit_path(gfx_surface_still(&node->content_icon,
gfx_display_texture_filter()),
texturepath, supports_rgba);
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);
}
}
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;
}
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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)
{
default: /* a value out of range in the config file */
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 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)
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
|| (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';
if ( settings->bools.playlist_show_entry_idx
&& (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;
bool show_entry_idx = true;
if (xmb->is_explore_list)
{
if ( string_is_equal(path, MENU_ENUM_LABEL_GOTO_EXPLORE_STR)
|| (xmb->depth == 1 && !string_is_equal(label, MENU_ENUM_LABEL_HORIZONTAL_MENU_STR)))
show_entry_idx = false;
else
{
/* Skip header items (Search Name + Add Additional Filter + Save as View + Delete this View) */
menu_entry_t entry;
MENU_ENTRY_INITIALIZE(entry);
menu_entry_get(&entry, 0, 0, NULL, true);
if (entry.type == MENU_SETTINGS_LAST + 1 || entry.type == FILE_TYPE_PLAIN)
entry_idx_offset = 1;
else if (entry.type == FILE_TYPE_RDB)
entry_idx_offset = 2;
}
if (entry_idx_selection > entry_idx_offset && entry_idx_offset)
entry_idx_selection -= entry_idx_offset;
else
show_entry_idx = false;
if (list_size >= entry_idx_offset)
list_size -= entry_idx_offset;
}
xmb->entry_index_offset = entry_idx_offset;
if (show_entry_idx)
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 && !menu_media_controls_settings_active(&xmb->media))
{
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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_U]); \
case RETRO_DEVICE_ID_JOYPAD_DOWN: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_D]); \
case RETRO_DEVICE_ID_JOYPAD_LEFT: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_L]); \
case RETRO_DEVICE_ID_JOYPAD_RIGHT: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_R]); \
case RETRO_DEVICE_ID_JOYPAD_B: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D]); \
case RETRO_DEVICE_ID_JOYPAD_A: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R]); \
case RETRO_DEVICE_ID_JOYPAD_Y: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_L]); \
case RETRO_DEVICE_ID_JOYPAD_X: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_U]); \
case RETRO_DEVICE_ID_JOYPAD_SELECT: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_SELECT]); \
case RETRO_DEVICE_ID_JOYPAD_START: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_START]); \
case RETRO_DEVICE_ID_JOYPAD_L: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_LB]); \
case RETRO_DEVICE_ID_JOYPAD_R: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_RB]); \
case RETRO_DEVICE_ID_JOYPAD_L2: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_LT]); \
case RETRO_DEVICE_ID_JOYPAD_R2: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_RT]); \
case RETRO_DEVICE_ID_JOYPAD_L3: \
case RETRO_DEVICE_ID_JOYPAD_R3: \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_STCK_P]); \
case 19: /* Left Analog Up */ \
case 23: /* Right Analog Up */ \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_STCK_U]); \
case 18: /* Left Analog Down */ \
case 22: /* Right Analog Down */ \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_STCK_D]); \
case 17: /* Left Analog Left */ \
case 21: /* Right Analog Left */ \
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_STCK_L]); \
case 16: /* Left Analog Right */ \
case 20: /* Right Analog Right */ \
return GFX_SURFACE_HANDLE(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, int input_turbo_bind, unsigned input_turbo_button)
{
switch (enum_idx)
{
case MENU_ENUM_LABEL_MAIN_MENU:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MAIN_MENU]);
case MENU_ENUM_LABEL_SETTINGS_TAB:
case MENU_ENUM_LABEL_SETTINGS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SETTINGS]);
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_OPTIONS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CORE_OPTIONS]);
case MENU_ENUM_LABEL_CORE_OPTION_OVERRIDE_LIST:
case MENU_ENUM_LABEL_REMAP_FILE_MANAGER_LIST:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_PLAYLIST]);
case MENU_ENUM_LABEL_CREATE_NEW_PLAYLIST:
case MENU_ENUM_LABEL_SIDELOAD_CORE_LIST:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_UNDO]);
case MENU_ENUM_LABEL_CORE_INPUT_REMAPPING_OPTIONS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_REMAPPING_OPTIONS]);
case MENU_ENUM_LABEL_CORE_CHEAT_OPTIONS:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_DISK_OPTIONS]);
case MENU_ENUM_LABEL_DISK_TRAY_EJECT:
case MENU_ENUM_LABEL_DISK_TRAY_INSERT:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RELOAD]);
case MENU_ENUM_LABEL_DISK_IMAGE_APPEND:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MENU_ADD]);
case MENU_ENUM_LABEL_SHADER_OPTIONS:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_SHADERS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SHADER_OPTIONS]);
case MENU_ENUM_LABEL_ACHIEVEMENT_LIST:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_LOADSTATE]);
case MENU_ENUM_LABEL_TAKE_SCREENSHOT:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_TAKE_SCREENSHOT:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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_PLAYLIST_MANAGER_REFRESH_ALL_PLAYLISTS:
case MENU_ENUM_LABEL_CLOUD_SYNC_SETTINGS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RELOAD]);
case MENU_ENUM_LABEL_VRR_RUNLOOP_ENABLE:
/* Only show icon in Throttle settings */
if (xmb->depth < 3)
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RELOAD]);
break;
case MENU_ENUM_LABEL_RENAME_ENTRY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RENAME]);
case MENU_ENUM_LABEL_RESUME_CONTENT:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_RESUME_CONTENT:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_ADD]);
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RDB]);
/* Menu collection submenus */
case MENU_ENUM_LABEL_PLAYLISTS_TAB:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_PLAYLIST]);
case MENU_ENUM_LABEL_LOAD_CONTENT_HISTORY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_HISTORY]);
case MENU_ENUM_LABEL_GOTO_FAVORITES:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FAVORITES]);
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
case MENU_ENUM_LABEL_GOTO_IMAGES:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_IMAGES]);
#endif
case MENU_ENUM_LABEL_GOTO_MUSIC:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MUSICS]);
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
case MENU_ENUM_LABEL_GOTO_VIDEO:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CURSOR]);
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RDB]);
case MENU_ENUM_LABEL_GOTO_CONTENTLESS_CORES:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_DISC]);
case MENU_ENUM_LABEL_CONTENT_SETTINGS:
case MENU_ENUM_LABEL_UPDATE_ASSETS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_QUICKMENU]);
case MENU_ENUM_LABEL_START_CORE:
case MENU_ENUM_LABEL_CHEAT_START_OR_CONT:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CLOSE]);
else if (strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MISSING_OPTIONAL)))
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FILE]);
case MENU_ENUM_LABEL_ONLINE_UPDATER:
case MENU_ENUM_LABEL_UPDATER_SETTINGS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_UPDATER]);
case MENU_ENUM_LABEL_UPDATE_LAKKA:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MAIN_MENU]);
case MENU_ENUM_LABEL_UPDATE_CHEATS:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_CHEATS:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RDB]);
case MENU_ENUM_LABEL_CURSOR_MANAGER_LIST:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CURSOR]);
case MENU_ENUM_LABEL_QUIT_RETROARCH:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_EXIT]);
/* Settings icons */
case MENU_ENUM_LABEL_DRIVER_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_DRIVERS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_DRIVERS]);
case MENU_ENUM_LABEL_VIDEO_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_VIDEO:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_VIDEO]);
case MENU_ENUM_LABEL_AUDIO_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_AUDIO:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_AUDIO]);
case MENU_ENUM_LABEL_AUDIO_MIXER_SETTINGS:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SETTING]);
case MENU_ENUM_LABEL_INPUT_SETTINGS:
case MENU_ENUM_LABEL_INPUT_INFORMATION:
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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS]);
case MENU_ENUM_LABEL_INPUT_MOUSE_INDEX:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_MOUSE]);
case MENU_ENUM_LABEL_INPUT_KEYBOARD_INDEX:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS]);
case MENU_ENUM_LABEL_INPUT_PLAYER_ANALOG_DPAD_MODE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_ADC]);
case MENU_ENUM_LABEL_INPUT_TURBO_FIRE_SETTINGS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_TURBO]);
case MENU_ENUM_LABEL_INPUT_TURBO_BIND:
case MENU_ENUM_LABEL_INPUT_TURBO_BUTTON:
{
int turbo_bind = input_turbo_bind;
if (enum_idx == MENU_ENUM_LABEL_INPUT_TURBO_BUTTON)
turbo_bind = (int)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:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SAVING]);
case MENU_ENUM_LABEL_LOGGING_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_LOGGING:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RECORD]);
case MENU_ENUM_LABEL_QUICK_MENU_START_STREAMING:
case MENU_ENUM_LABEL_QUICK_MENU_SHOW_START_STREAMING:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FILE]);
case MENU_ENUM_LABEL_CORE_LOCK:
case MENU_ENUM_LABEL_CORE_SET_STANDALONE_EXEMPT:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CORE]);
case MENU_ENUM_LABEL_ONSCREEN_DISPLAY_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_ONSCREEN_DISPLAY:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_UI]);
#if defined(HAVE_LIBNX)
case MENU_ENUM_LABEL_SWITCH_CPU_PROFILE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_POWER]);
#endif
case MENU_ENUM_LABEL_POWER_MANAGEMENT_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_POWER_MANAGEMENT:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_POWER]);
case MENU_ENUM_LABEL_RETRO_ACHIEVEMENTS_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_ACHIEVEMENTS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_ACHIEVEMENTS]);
case MENU_ENUM_LABEL_PLAYLIST_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_PLAYLISTS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_PLAYLIST]);
case MENU_ENUM_LABEL_USER_SETTINGS:
case MENU_ENUM_LABEL_SETTINGS_SHOW_USER:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_USER]);
case MENU_ENUM_LABEL_PRIVACY_SETTINGS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_PRIVACY]);
case MENU_ENUM_LABEL_REWIND_SETTINGS:
case MENU_ENUM_LABEL_CONTENT_SHOW_REWIND:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_OVERRIDE]);
case MENU_ENUM_LABEL_ONSCREEN_NOTIFICATIONS_SETTINGS:
case MENU_ENUM_LABEL_DISPLAY_INFORMATION:
case MENU_ENUM_LABEL_CHEEVOS_APPEARANCE_SETTINGS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_NOTIFICATIONS]);
#ifdef HAVE_NETWORKING
case MENU_ENUM_LABEL_NETPLAY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_NETPLAY]);
case MENU_ENUM_LABEL_NETPLAY_ENABLE_HOST:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RUN]);
case MENU_ENUM_LABEL_NETPLAY_DISCONNECT:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CLOSE]);
case MENU_ENUM_LABEL_NETPLAY_ENABLE_CLIENT:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_ROOM]);
case MENU_ENUM_LABEL_NETPLAY_REFRESH_ROOMS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RELOAD]);
#ifdef HAVE_NETPLAYDISCOVERY
case MENU_ENUM_LABEL_NETPLAY_REFRESH_LAN:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RELOAD]);
#endif
case MENU_ENUM_LABEL_NETPLAY_REQUEST_DEVICES:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SUBSETTING]);
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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_NETWORK]);
#endif
case MENU_ENUM_LABEL_BLUETOOTH_SETTINGS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_BLUETOOTH]);
case MENU_ENUM_LABEL_SHUTDOWN:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SHUTDOWN]);
case MENU_ENUM_LABEL_CHEAT_APPLY_CHANGES:
case MENU_ENUM_LABEL_SHADER_APPLY_CHANGES:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MENU_ADD]);
case MENU_ENUM_LABEL_CHEAT_APPLY_AFTER_TOGGLE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MENU_APPLY_TOGGLE]);
case MENU_ENUM_LABEL_CHEAT_APPLY_AFTER_LOAD:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MENU_APPLY_COG]);
case MENU_ENUM_LABEL_SLOWMOTION_RATIO:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RESUME]);
case MENU_ENUM_LABEL_START_VIDEO_PROCESSOR:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SAVING]);
else if (string_is_equal(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_EXPLORE_DELETE_VIEW)))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CLOSE]);
else if (type != FILE_TYPE_RDB)
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FOLDER]);
case FILE_TYPE_PLAIN:
case FILE_TYPE_IN_CARCHIVE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FILE]);
case FILE_TYPE_RPL_ENTRY:
case FILE_TYPE_PLAYLIST_COLLECTION:
if (core_node)
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(db_node->content_icon);
case FILE_TYPE_PLAYLIST_COLLECTION:
return GFX_SURFACE_HANDLE(db_node->icon);
default:
break;
}
}
else
{
if (string_is_equal(xmb->title_name, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_IMAGES_TAB)))
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MOVIE]);
}
}
break;
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
case XMB_SYSTEM_TAB_IMAGES:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_IMAGE]);
#endif
case XMB_SYSTEM_TAB_MUSIC:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MUSIC]);
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
case XMB_SYSTEM_TAB_VIDEO:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MOVIE]);
#endif
default:
break;
}
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FILE]);
case MENU_SET_CDROM_INFO:
case MENU_SET_CDROM_LIST:
case MENU_SET_LOAD_CDROM_LIST:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_DISC]);
case FILE_TYPE_SHADER:
case FILE_TYPE_SHADER_PRESET:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SHADER_OPTIONS]);
case FILE_TYPE_USE_DIRECTORY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CHECKMARK]);
case FILE_TYPE_CARCHIVE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_ZIP]);
case FILE_TYPE_IMAGE:
case FILE_TYPE_IMAGEVIEWER:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_IMAGE]);
case FILE_TYPE_MUSIC:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MUSIC]);
case FILE_TYPE_MOVIE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_MOVIE]);
case FILE_TYPE_CORE:
case FILE_TYPE_DOWNLOAD_CORE:
case FILE_TYPE_DIRECT_LOAD:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CORE]);
case FILE_TYPE_RDB:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RDB]);
case FILE_TYPE_RDB_ENTRY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FILE]);
case FILE_TYPE_PLAYLIST_ENTRY:
case MENU_SETTING_ACTION_RUN:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RUN]);
case MENU_SETTING_ACTION_RESUME_ACHIEVEMENTS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RESUME]);
case MENU_SETTING_ACTION_CLOSE:
case MENU_SETTING_ACTION_CLOSE_HORIZONTAL:
case MENU_SETTING_ACTION_DELETE_ENTRY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CLOSE]);
case MENU_SETTING_ACTION_SAVESTATE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SAVESTATE]);
case MENU_SETTING_ACTION_LOADSTATE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_LOADSTATE]);
case MENU_SETTING_ACTION_PLAYREPLAY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_PLAYREPLAY]);
case MENU_SETTING_ACTION_RECORDREPLAY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RECORDREPLAY]);
case MENU_SETTING_ACTION_HALTREPLAY:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_AUDIO]);
else if (string_starts_with(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_INPUT_SETTINGS)))
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_POWER]);
else if (strstr(enum_path, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_MAPPING_SETTINGS)))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_OVERRIDE]);
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CORE_OPTIONS]);
case MENU_SETTING_ACTION_CORE_INPUT_REMAPPING_OPTIONS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_REMAPPING_OPTIONS]);
case MENU_SETTING_ACTION_CORE_CHEAT_OPTIONS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CHEAT_OPTIONS]);
case MENU_SETTING_ACTION_CORE_DISK_OPTIONS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_DISK_OPTIONS]);
case MENU_SETTING_ACTION_CORE_SHADER_OPTIONS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SHADER_OPTIONS]);
case MENU_SETTING_ACTION_SCREENSHOT:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SCREENSHOT]);
case MENU_SETTING_ACTION_RESET:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RELOAD]);
case MENU_SETTING_ACTION_PAUSE_ACHIEVEMENTS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_PAUSE]);
case MENU_ENUM_LABEL_ACHIEVEMENT_SERVER_UNREACHABLE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_NETWORK]);
case MENU_SET_SCREEN_BRIGHTNESS:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_BRIGHTNESS]);
case MENU_SETTING_GROUP:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SETTING]);
case MENU_INFO_MESSAGE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CORE_INFO]);
case MENU_BLUETOOTH:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_BLUETOOTH]);
case MENU_WIFI:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_WIFI]);
#ifdef HAVE_NETWORKING
case MENU_ROOM:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_ROOM]);
case MENU_ROOM_LAN:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_ROOM_LAN]);
case MENU_ROOM_RELAY:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_ROOM_RELAY]);
#endif
case MENU_SETTINGS_INPUT_LIBRETRO_DEVICE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS]);
case MENU_SETTINGS_INPUT_ANALOG_DPAD_MODE:
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_ADC]);
case MENU_SETTINGS_INPUT_INPUT_REMAP_PORT:
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INFO]);
/* Placeholder badge image was not found, show generic menu icon */
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_MOUSE]);
else if (string_ends_with_size(enum_label, "_keyboard_index", enum_label_len, STRLEN_CONST("_keyboard_index")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FILE]);
else if (string_ends_with_size(enum_label, "_analog_dpad_mode", enum_label_len, STRLEN_CONST("_analog_dpad_mode")))
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_RELOAD]);
else if (string_ends_with_size(enum_label, "_save_autoconfig", enum_label_len, STRLEN_CONST("_save_autoconfig")))
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_TURBO]);
else if (strstr(enum_label, "_gun_")
|| strstr(enum_label, "_aim_"))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_LGUN]);
else if (string_starts_with_size(enum_label, "input_device_reserv", STRLEN_CONST("input_device_reserv")))
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_U]);
else if (string_ends_with_size(enum_label, "_down", enum_label_len, STRLEN_CONST("_down")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_D]);
else if (string_ends_with_size(enum_label, "_left", enum_label_len, STRLEN_CONST("_left")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_L]);
else if (string_ends_with_size(enum_label, "_right", enum_label_len, STRLEN_CONST("_right")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_DPAD_R]);
else if (string_ends_with_size(enum_label, "_b", enum_label_len, STRLEN_CONST("_b")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_D]);
else if (string_ends_with_size(enum_label, "_a", enum_label_len, STRLEN_CONST("_a")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_R]);
else if (string_ends_with_size(enum_label, "_y", enum_label_len, STRLEN_CONST("_y")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_L]);
else if (string_ends_with_size(enum_label, "_x", enum_label_len, STRLEN_CONST("_x")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_BTN_U]);
else if (string_ends_with_size(enum_label, "_select", enum_label_len, STRLEN_CONST("_select")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_SELECT]);
else if (string_ends_with_size(enum_label, "_start", enum_label_len, STRLEN_CONST("_start")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_START]);
else if (string_ends_with_size(enum_label, "_l", enum_label_len, STRLEN_CONST("_l")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_LB]);
else if (string_ends_with_size(enum_label, "_r", enum_label_len, STRLEN_CONST("_r")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_RB]);
else if (string_ends_with_size(enum_label, "_l2", enum_label_len, STRLEN_CONST("_l2")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_LT]);
else if (string_ends_with_size(enum_label, "_r2", enum_label_len, STRLEN_CONST("_r2")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_RT]);
else if (string_ends_with_size(enum_label, "_l3", enum_label_len, STRLEN_CONST("_l3")))
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_STCK_P]);
else if (string_ends_with_size(enum_label, "_r3", enum_label_len, STRLEN_CONST("_r3")))
return GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(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 GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_STCK_R]);
}
if ( type >= MENU_SETTINGS_REMAPPING_PORT_BEGIN
&& type <= MENU_SETTINGS_REMAPPING_PORT_END)
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_INPUT_SETTINGS]);
if (checked)
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CHECKMARK]);
if (type == MENU_SETTING_ACTION)
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SETTING]);
return GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SUBSETTING]);
}
/* 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;
const video_frame_info_t *video_info;
math_matrix_4x4 *mymat;
xmb_handle_t *xmb;
xmb_node_t *core_node;
file_list_t *list;
float *color;
/* The output size, both axes in one word,
* VIDEO_SCALE_PACK's layout. */
unsigned video_dims;
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;
const video_frame_info_t *video_info = ctx->video_info;
bool shadows_enable = ctx->shadows_enable;
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_dims = VIDEO_SCALE_PACK(
xmb->font2_size * 0.5f * line_ticker_width,
(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;
gfx_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;
line_ticker.wrap_scratch = &xmb->wrap_scratch;
line_ticker.wrap_scratch_cap = &xmb->wrap_scratch_cap;
gfx_animation_line_ticker(ctx->p_anim, &line_ticker);
}
/* Draw sublabel */
xmb_draw_text(shadows_enable, xmb, video_info,
entry_sublabel,
sublabel_x,
ticker_y_offset + sublabel_y,
1, node->label_alpha * xmb->alpha_list, TEXT_ALIGN_LEFT,
ctx->video_dims, 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, video_info,
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,
ctx->video_dims, xmb->font2);
if ( *entry_sublabel_bottom_fade
&& ticker_bottom_fade_alpha > 0.0f)
xmb_draw_text(shadows_enable, xmb, video_info,
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,
ctx->video_dims, 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. */
const video_frame_info_t *video_info = ctx->video_info;
xmb_handle_t *xmb = ctx->xmb;
file_list_t *list = ctx->list;
float *color = ctx->color;
unsigned video_dims = ctx->video_dims;
unsigned width = VIDEO_SCALE_W(video_dims);
unsigned height = VIDEO_SCALE_H(video_dims);
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 = ((video_info->menu.flags & VIDEO_MENU_FLAG_TICKER_SMOOTH) ? true : false);
enum gfx_animation_ticker_type menu_ticker_type
= (enum gfx_animation_ticker_type)video_info->menu.ticker_type;
unsigned thumbnail_scale_factor = video_info->menu.xmb_thumbnail_scale_factor;
bool vertical_thumbnails = ((video_info->menu.flags & VIDEO_MENU_FLAG_XMB_VERTICAL_THUMBNAILS) ? true : false);
bool show_sublabels = ((video_info->menu.flags & VIDEO_MENU_FLAG_SHOW_SUBLABELS) ? true : false);
bool show_entry_icons = ((video_info->menu.flags & VIDEO_MENU_FLAG_XMB_ENTRY_ICONS) ? true : false);
bool show_switch_icons = ((video_info->menu.flags & VIDEO_MENU_FLAG_XMB_SWITCH_ICONS) ? true : false);
unsigned show_history_icons = video_info->menu.playlist_show_history_icons;
unsigned vertical_fade_factor = video_info->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)];
/* Source from entry.path so the copy never overlaps its own
* destination; fortified strlcpy() traps on overlap. */
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 (GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SWITCH_OFF]))
texture_switch = GFX_SURFACE_HANDLE(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 (GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_SWITCH_ON]))
texture_switch = GFX_SURFACE_HANDLE(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 && ((video_info->menu.flags & VIDEO_MENU_FLAG_XMB_FONT_IS_DEFAULT) ? true : false))
{
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_index_str)
{
float entry_idx_margin = 12 * xmb->last_scale_factor;
float x_position = width - entry_idx_margin;
float y_position = height - entry_idx_margin;
xmb_draw_text(shadows_enable, xmb, video_info,
xmb->entry_index_str, x_position, y_position,
1, vertical_thumbnails ? node->label_alpha : 1,
TEXT_ALIGN_RIGHT, video_dims, xmb->font);
}
/* Entry label */
xmb_draw_text(shadows_enable, xmb, video_info, 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,
video_dims, 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, video_info, 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,
video_dims, 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,
video_info->menu.input_turbo_bind,
video_info->menu.input_turbo_button);
float scale_factor = node->zoom;
if (i != current)
{
/* Differentiate the basic setting icon from the rest */
if (texture == GFX_SURFACE_HANDLE(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 = GFX_SURFACE_HANDLE(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 = GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_FAVORITES]);
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
else if (string_is_equal(entry.rich_label, msg_hash_to_str(MENU_ENUM_LABEL_VALUE_IMAGES_TAB)))
texture = GFX_SURFACE_HANDLE(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 = GFX_SURFACE_HANDLE(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 = GFX_SURFACE_HANDLE(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;
}
/* Search loop misses leave offset == horizontal_list.size;
* indexing the list with it reads out of bounds */
sidebar_node = (offset < xmb->horizontal_list.size)
? (xmb_node_t*)file_list_get_userdata_at_offset(&xmb->horizontal_list, offset)
: NULL;
if (sidebar_node && GFX_SURFACE_HANDLE(sidebar_node->icon))
texture = GFX_SURFACE_HANDLE(sidebar_node->icon);
}
}
/* "Main Menu" playlists */
else if (xmb->depth == 2 && entry_type == FILE_TYPE_PLAYLIST_COLLECTION)
{
xmb_node_t *sidebar_node = NULL;
size_t offset = list->list[i].entry_idx;
/* Search for sorted icon order */
if (((video_info->menu.flags & VIDEO_MENU_FLAG_OZONE_SORT_AFTER_TRUNCATE_PLAYLIST_NAME) ? true : false))
{
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;
}
}
/* offset is either the entry's untrusted entry_idx or a
* search-loop result that equals horizontal_list.size on a
* miss - both can point past the end of the list, so it must
* be bounds-checked, not merely the list checked as
* non-empty */
sidebar_node = (offset < xmb->horizontal_list.size)
? (xmb_node_t*)file_list_get_userdata_at_offset(&xmb->horizontal_list, offset)
: NULL;
if (sidebar_node && GFX_SURFACE_HANDLE(sidebar_node->icon))
texture = GFX_SURFACE_HANDLE(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 = GFX_SURFACE_HANDLE(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 = GFX_SURFACE_HANDLE(db_node->icon);
break;
case PLAYLIST_SHOW_HISTORY_ICONS_CONTENT:
texture = GFX_SURFACE_HANDLE(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,
VIDEO_SCALE_PACK(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_dims,
(show_icon_thumbnail) ? false : shadows_enable,
gfx_icon_width,
gfx_icon_height,
texture,
gfx_icon_x,
gfx_icon_y,
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 == GFX_SURFACE_HANDLE(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_dims,
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,
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
&& video_info->menu.xmb_current_menu_icon != XMB_CURRENT_MENU_ICON_NORMAL)
? GFX_SURFACE_HANDLE(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 = GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_CHECKMARK]);
}
if (tex)
{
xmb_draw_icon(
ctx->userdata,
ctx->p_disp,
ctx->dispctx,
video_dims,
shadows_enable,
icon_size,
icon_size,
tex,
current_x,
current_y,
node->alpha,
(entry.flags & MENU_ENTRY_FLAG_CHECKED) ? 0 : M_PI,
0.5f,
&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, video_info,
">",
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_dims,
xmb->font);
if (entry.flags & MENU_ENTRY_FLAG_CHECKED)
xmb_draw_text(shadows_enable, xmb, video_info,
"|",
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_dims,
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,
const video_frame_info_t *video_info,
unsigned video_dims,
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)
{
unsigned video_height = VIDEO_SCALE_H(video_dims);
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.video_info = video_info;
ctx.mymat = mymat;
ctx.xmb = xmb;
ctx.core_node = NULL;
ctx.list = list;
ctx.color = color;
ctx.video_dims = video_dims;
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 dims)
{
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 (VIDEO_SCALE_W(dims) > 320) && (VIDEO_SCALE_H(dims) > 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 *data,
struct menu_state *menu_st, size_t selection);
static void xmb_media_settings_show(void *user)
{
struct menu_state *menu_st = menu_state_get_ptr();
xmb_show_fullscreen_thumbnails((xmb_handle_t*)user,
menu_st, menu_st->selection_ptr);
}
static void xmb_media_settings_hide(void *user)
{
xmb_handle_t *xmb = (xmb_handle_t*)user;
xmb_hide_fullscreen_thumbnails(xmb, false);
}
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 && !menu_driver_media_startup_token())
{
/* 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;
if ((xmb->is_file_list
|| (xmb->is_playlist && action == MENU_ACTION_OK
&& menu_media_selected_in_history(menu_st)))
&& !xmb->show_fullscreen_thumbnails
&& (action == MENU_ACTION_OK || action == MENU_ACTION_SELECT || action == MENU_ACTION_START)
&& menu_media_open_selected_file(menu_st, &xmb->thumbnails.left, &xmb->thumbnails.right))
{
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
xmb->fullscreen_thumbnails_available = true;
xmb_show_fullscreen_thumbnails(xmb, menu_st, menu_st->selection_ptr);
return MENU_ACTION_NOOP;
}
if (xmb->show_fullscreen_thumbnails
&& menu_media_controls_action(&xmb->media, action,
menu_driver_get_current_time()))
return MENU_ACTION_NOOP;
if (xmb->show_fullscreen_thumbnails && action == MENU_ACTION_OK
&& ((xmb->is_file_list && menu_media_selected_is_viewable(menu_st))
|| (xmb->is_playlist && menu_media_selected_in_history(menu_st))))
return MENU_ACTION_NOOP;
/* 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 = menu_st->selection_ptr;
size_t new_selection = menu_playlists_tab_random_selection(selection);
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_hold_until = menu_driver_get_current_time()
+ 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)
{
file_list_set_label_at_offset(menu_stack,
stack_size - 1, 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 = VIDEO_PX(64.0f * scale_factor);
xmb->icon_size = VIDEO_PX(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";
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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_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_texture_file_path(unsigned i, void *ud, char *buf,
size_t len)
{
fill_pathname_join_special(buf, (const char*)ud, xmb_texture_path(i), len);
}
static void xmb_context_reset_textures(
xmb_handle_t *xmb,
const char *iconpath,
unsigned menu_xmb_theme)
{
/* The set decoded across the cores, then up in order */
gfx_surface_submit_named(xmb->textures.list, XMB_TEXTURE_LAST,
gfx_display_texture_filter(), xmb_texture_file_path,
(void*)iconpath, gfx_surface_wants_rgba());
/* The tabs draw the list's stills */
xmb->main_menu_node.icon = xmb->textures.list[XMB_TEXTURE_MAIN_MENU];
xmb->settings_tab_node.icon = xmb->textures.list[XMB_TEXTURE_SETTINGS];
xmb->history_tab_node.icon = xmb->textures.list[XMB_TEXTURE_HISTORY];
xmb->favorites_tab_node.icon = xmb->textures.list[XMB_TEXTURE_FAVORITES];
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
xmb->images_tab_node.icon = xmb->textures.list[XMB_TEXTURE_IMAGES];
#endif
xmb->music_tab_node.icon = xmb->textures.list[XMB_TEXTURE_MUSICS];
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
xmb->video_tab_node.icon = xmb->textures.list[XMB_TEXTURE_MOVIES];
#endif
xmb->add_tab_node.icon = xmb->textures.list[XMB_TEXTURE_ADD];
xmb->contentless_cores_tab_node.icon = xmb->textures.list[XMB_TEXTURE_CORE];
#if defined(HAVE_LIBRETRODB)
xmb->explore_tab_node.icon = xmb->textures.list[XMB_TEXTURE_RDB];
#endif
#ifdef HAVE_NETWORKING
xmb->netplay_tab_node.icon = xmb->textures.list[XMB_TEXTURE_NETPLAY];
#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;
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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)
{
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);
}
}
if (xmb->show_fullscreen_thumbnails
&& (xmb->is_file_list
|| (xmb->is_playlist && menu_media_selected_in_history(menu_st)))
&& menu_media_open_selected_file(menu_st,
&xmb->thumbnails.left, &xmb->thumbnails.right))
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
/* 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 dims, bool is_idle)
{
int startup_token = menu_driver_media_startup_token();
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;
menu_media_controls_settings_iterate(&xmb->media);
if (startup_token && xmb->thumbnails.pending == XMB_PENDING_THUMBNAIL_NONE
&& (xmb->thumbnails.left.status == GFX_THUMBNAIL_STATUS_MISSING
|| xmb->thumbnails.right.status == GFX_THUMBNAIL_STATUS_MISSING)
&& xmb->thumbnails.left.status != GFX_THUMBNAIL_STATUS_PENDING
&& xmb->thumbnails.right.status != GFX_THUMBNAIL_STATUS_PENDING
&& xmb->thumbnails.left.status != GFX_THUMBNAIL_STATUS_AVAILABLE
&& xmb->thumbnails.right.status != GFX_THUMBNAIL_STATUS_AVAILABLE)
menu_driver_media_startup_ready(startup_token);
if (!menu_media_controls_settings_active(&xmb->media)
&& xmb->is_file_list && !xmb->show_fullscreen_thumbnails
&& !gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_RIGHT)
&& !gfx_thumbnail_is_enabled(menu_st->thumbnail_path_data, GFX_THUMBNAIL_LEFT)
&& (xmb->thumbnails.left.anim_player || xmb->thumbnails.right.anim_player))
{
gfx_thumbnail_reset(&xmb->thumbnails.left);
gfx_thumbnail_reset(&xmb->thumbnails.right);
}
/* Advance animated thumbnails (animated WebP) once per frame on the
* main thread. No-op for still images. */
gfx_thumbnail_animate(&xmb->thumbnails.right,
menu_driver_get_current_time());
gfx_thumbnail_animate(&xmb->thumbnails.left,
menu_driver_get_current_time());
gfx_thumbnail_animate(&xmb->thumbnails.icon,
menu_driver_get_current_time());
/* 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() clears the deadline on success and lets it
* lapse via xmb_icon_retry_next() otherwise, so the loop
* terminates either way. */
if (xmb->current_menu_icon_retry_until)
xmb_set_title(xmb);
use_ps3_layout = xmb_use_ps3_layout(settings->uints.menu_xmb_layout, dims);
scale_factor = xmb_get_scale_factor(settings->floats.menu_scale_factor,
use_ps3_layout, VIDEO_SCALE_W(dims));
#if defined(_WIN32) && !defined(_XBOX) && !defined(__WINRT__)
if (xmb->last_scale_factor > 0.0f && win32_message_modal_active())
{
scale_factor = xmb->last_scale_factor;
use_ps3_layout = xmb->last_use_ps3_layout;
}
#endif
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_dims = dims;
/* Read pointer state */
menu_input_get_pointer_state(&xmb->pointer);
xmb->media.settings_user = xmb;
xmb->media.settings_hide = xmb_media_settings_hide;
xmb->media.settings_show = xmb_media_settings_show;
menu_media_controls_thumbnail(&xmb->media, &xmb->thumbnails.left,
&xmb->thumbnails.right,
xmb->show_fullscreen_thumbnails && !xmb->show_screensaver
&& !menu_input_dialog_get_display_kb(),
VIDEO_SCALE_W(dims), VIDEO_SCALE_H(dims),
&xmb->pointer, menu_driver_get_current_time());
/* Direction locking for horizontal category dragging */
if (xmb->pointer.type != MENU_POINTER_DISABLED
&& !xmb->media.controls.target.user)
{
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)VIDEO_POS_X(xmb->pointer.pos) - xmb->drag_start_x);
float dy = fabs((float)VIDEO_POS_Y(xmb->pointer.pos) - 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)VIDEO_POS_X(xmb->pointer.pos) - 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)VIDEO_POS_Y(xmb->pointer.pos) - 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,
dims,
settings->paths.directory_assets);
GFX_ANIMATION_CLEAR_ACTIVE(p_anim);
return;
}
if (xmb->pointer.type != MENU_POINTER_DISABLED
&& !xmb->media.controls.target.user)
{
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)VIDEO_SCALE_W(dims) - xmb->margins_screen_left);
int16_t pointer_x = VIDEO_POS_X(xmb->pointer.pos);
int16_t pointer_y = VIDEO_POS_Y(xmb->pointer.pos);
/* 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 (VIDEO_SCALE_H(dims))
xmb_calculate_visible_range(xmb, VIDEO_SCALE_H(dims),
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 = &xmb->render_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 (VIDEO_SCALE_H(dims))
xmb_calculate_visible_range(xmb, VIDEO_SCALE_H(dims), 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;
/* Every selection change bumps the thumbnail generation and
* cancels the in-flight loads, so an entry that was PENDING on
* the previous step is now PENDING with nothing behind it: its
* task was cancelled and gfx_thumbnail_handle_upload will not
* touch it through the stale tag. Recover it here, before the
* status early-out below, or it is skipped for as long as it
* stays on screen and the icon never appears (#19075: hold
* Down in a playlist and some icons stay blank). */
gfx_thumbnail_reset_if_orphaned(&thumbnail_icon->icon);
/* 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 = (selection < selection_buf->size)
? (xmb_node_t*)selection_buf->list[selection].userdata
: NULL;
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);
/* The frame's thumbnail requests go out together */
gfx_thumbnail_flush_requests();
}
XMB_NOINLINE static void xmb_draw_bg(
void *userdata,
gfx_display_t *p_disp,
gfx_display_ctx_driver_t *dispctx,
const gfx_display_mesh_t *ribbon_mesh,
unsigned video_dims,
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)
{
unsigned video_width = VIDEO_SCALE_W(video_dims);
unsigned video_height = VIDEO_SCALE_H(video_dims);
gfx_display_ctx_draw_t draw;
struct video_coords coords;
draw.pos = VIDEO_POS_PACK(0, 0);
draw.texture = texture_id;
draw.dims = video_dims;
draw.color = &coord_black[0];
draw.vertex = NULL;
draw.tex_coord = NULL;
draw.vertex_count = 4;
draw.pipeline_id = 0;
if (!video_width || !video_height)
return;
gfx_display_blend_begin(dispctx, 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);
gfx_display_draw(dispctx, &draw, userdata,
video_dims);
}
/* 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);
gfx_display_draw(dispctx, &draw, userdata,
video_dims);
}
#ifdef HAVE_SHADERPIPELINE
/* Draw pipeline */
if (menu_shader_pipeline > XMB_SHADER_PIPELINE_WALLPAPER)
{
gfx_display_mesh_draw_t md;
const gfx_display_mesh_t *mesh = gfx_display_mesh_fullscreen();
switch (menu_shader_pipeline)
{
default:
case XMB_SHADER_PIPELINE_WALLPAPER:
md.program = GFX_MESH_PROGRAM_BLEND;
break;
case XMB_SHADER_PIPELINE_RIBBON:
md.program = GFX_MESH_PROGRAM_RIBBON;
mesh = ribbon_mesh;
break;
case XMB_SHADER_PIPELINE_SIMPLE_RIBBON:
md.program = GFX_MESH_PROGRAM_RIBBON_SIMPLE;
mesh = ribbon_mesh;
break;
#if !defined(VITA)
case XMB_SHADER_PIPELINE_SIMPLE_SNOW:
md.program = GFX_MESH_PROGRAM_SNOW_SIMPLE;
break;
case XMB_SHADER_PIPELINE_SNOW:
md.program = GFX_MESH_PROGRAM_SNOW;
break;
case XMB_SHADER_PIPELINE_BOKEH:
md.program = GFX_MESH_PROGRAM_BOKEH;
break;
case XMB_SHADER_PIPELINE_SNOWFLAKE:
md.program = GFX_MESH_PROGRAM_SNOWFLAKE;
break;
#endif
}
md.mvp = NULL;
md.color = draw.color;
md.texture = draw.texture;
gfx_display_mesh_draw(p_disp, userdata, video_dims, mesh, &md);
}
#endif
gfx_display_blend_end(dispctx, 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 video_dims,
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.pos = VIDEO_POS_PACK(0, 0);
draw.dims = video_dims;
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;
gfx_display_blend_begin(dispctx, userdata);
gfx_display_draw_bg(p_disp, &draw, &coords, userdata, true, MIN(xmb->alpha, alpha));
if (VIDEO_SCALE_H(draw.dims) > 0 && VIDEO_SCALE_W(draw.dims) > 0)
gfx_display_draw(dispctx, &draw, userdata, video_dims);
gfx_display_blend_end(dispctx, userdata);
}
static void xmb_draw_no_thumbnail_available(
xmb_handle_t *xmb,
const video_frame_info_t *video_info,
gfx_display_t *p_disp,
void *userdata,
unsigned video_dims,
unsigned x_position,
unsigned y_position,
unsigned view_width,
unsigned view_height,
bool shadows_enable,
bool draw_text,
float *color,
math_matrix_4x4 *mymat)
{
unsigned video_height = VIDEO_SCALE_H(video_dims);
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);
gfx_display_blend_begin(dispctx, userdata);
if (dispctx->draw)
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_dims,
false,
icon_size,
icon_size,
GFX_SURFACE_HANDLE(xmb->textures.list[XMB_TEXTURE_IMAGE]),
x_position + ((view_width - icon_size) / 2),
y_position + ((view_height - icon_size) / 2) + icon_size,
xmb->alpha,
0,
1,
&color[0],
xmb->shadow_offset / 2,
mymat);
gfx_display_blend_end(dispctx, userdata);
}
if (!draw_text)
return;
xmb_draw_text(shadows_enable, xmb, video_info,
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_dims,
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_dims,
bool shadows_enable,
unsigned xmb_color_theme,
float *color,
const video_frame_info_t *video_info, size_t selection)
{
unsigned video_width = VIDEO_SCALE_W(video_dims);
unsigned video_height = VIDEO_SCALE_H(video_dims);
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;
int startup_token = menu_driver_media_startup_token();
if (startup_token
&& xmb->fullscreen_thumbnail_alpha >= 0.9999f
&& xmb->show_fullscreen_thumbnails
&& ((xmb->thumbnails.left.status == GFX_THUMBNAIL_STATUS_AVAILABLE
&& xmb->thumbnails.left.alpha >= 0.9999f)
|| (xmb->thumbnails.right.status == GFX_THUMBNAIL_STATUS_AVAILABLE
&& xmb->thumbnails.right.alpha >= 0.9999f)))
menu_driver_media_startup_ready(startup_token);
/* 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 = ((video_info->menu.flags & VIDEO_MENU_FLAG_TICKER_SMOOTH) ? true : false);
enum gfx_animation_ticker_type menu_ticker_type
= (enum gfx_animation_ticker_type)video_info->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,
VIDEO_SCALE_PACK(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,
VIDEO_SCALE_PACK(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_dims,
0,
0,
VIDEO_SCALE_PACK((unsigned)view_width, (unsigned)view_height),
VIDEO_SCALE_PACK((unsigned)view_width, (unsigned)view_height),
background_color,
NULL);
/* Draw header */
if (show_header)
{
/* Background */
gfx_display_draw_quad(
p_disp,
userdata,
video_dims,
0,
0,
VIDEO_SCALE_PACK((unsigned)view_width, (unsigned)header_height),
VIDEO_SCALE_PACK((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,
VIDEO_SCALE_PACK(view_width, 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,
VIDEO_SCALE_PACK(view_width, view_height),
title_color,
TEXT_ALIGN_CENTER,
1.0f, false, 0.0f, false);
}
/* Draw thumbnails */
/* > No shadow effect: the thumbnails already have a background
* border, so a drop shadow behind one only muddies its edge */
thumbnail_shadow.type = GFX_THUMBNAIL_SHADOW_NONE;
/* > Right */
if (show_right_thumbnail)
{
/* Background */
gfx_display_draw_quad(
p_disp,
userdata,
video_dims,
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),
VIDEO_SCALE_PACK(
(unsigned)right_thumbnail_draw_width + (frame_width << 1),
(unsigned)right_thumbnail_draw_height + (frame_width << 1)),
VIDEO_SCALE_PACK((unsigned)view_width, (unsigned)view_height),
frame_color,
NULL);
/* Thumbnail */
gfx_thumbnail_draw(
userdata,
video_dims,
right_thumbnail,
right_thumbnail_x,
thumbnail_y,
VIDEO_SCALE_PACK((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_dims,
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),
VIDEO_SCALE_PACK(
(unsigned)left_thumbnail_draw_width + (frame_width << 1),
(unsigned)left_thumbnail_draw_height + (frame_width << 1)),
VIDEO_SCALE_PACK((unsigned)view_width, (unsigned)view_height),
frame_color,
NULL);
/* Thumbnail */
gfx_thumbnail_draw(
userdata,
video_dims,
left_thumbnail,
left_thumbnail_x,
thumbnail_y,
VIDEO_SCALE_PACK((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,
video_info,
p_disp,
userdata,
video_dims,
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;
float thumbnail_scale_factor = (float)video_info->menu.xmb_thumbnail_scale_factor / 100.0f;
bool menu_core_enable = ((video_info->menu.flags & VIDEO_MENU_FLAG_CORE_ENABLE) ? true : false);
bool show_title_header = ((video_info->menu.flags & VIDEO_MENU_FLAG_XMB_SHOW_TITLE_HEADER) ? true : false);
bool vertical_thumbnails = ((video_info->menu.flags & VIDEO_MENU_FLAG_XMB_VERTICAL_THUMBNAILS) ? true : false);
unsigned vertical_fade_factor = video_info->menu.xmb_vertical_fade_factor;
unsigned current_menu_icon = video_info->menu.xmb_current_menu_icon;
void *userdata = video_info->userdata;
unsigned video_width = VIDEO_SCALE_W(video_info->dims);
unsigned video_height = VIDEO_SCALE_H(video_info->dims);
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_dims = video_info->dims;
/* 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 = GFX_SURFACE_HANDLE(xmb->textures.bg);
for (i = 0; i < XMB_TEXTURE_LAST; i++)
tex_list[i] = GFX_SURFACE_HANDLE(xmb->textures.list[i]);
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_info->dims, 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)video_info->menu.xmb_title_margin * 10.0f;
xmb->margins_title_horizontal_offset = (float)video_info->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_hold_until
&& menu_driver_get_current_time() >= xmb->draw_entry_hold_until)
{
xmb->draw_entry_hold_until = 0;
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_info->dims,
0,
0,
video_info->dims,
video_info->dims,
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,
xmb->ribbon_mesh,
video_info->dims,
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);
}
gfx_display_blend_begin(dispctx, userdata);
/* List icons */
xmb_draw_items(
userdata,
p_disp,
dispctx,
p_anim,
menu_st,
video_info,
video_info->dims,
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)
&& video_info->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,
VIDEO_SCALE_PACK(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_info->dims,
icon_thumbnail,
gfx_icon_x,
gfx_icon_y,
VIDEO_SCALE_PACK(gfx_icon_width, gfx_icon_height),
GFX_THUMBNAIL_ALIGN_CENTRE,
1.0f,
1.0f,
&thumbnail_shadow);
}
}
gfx_display_blend_end(dispctx, userdata);
gfx_display_blend_begin(dispctx, 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_info->dims,
shadows_enable,
icon_size,
icon_size,
xmb->current_menu_icon,
current_x,
current_y,
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_info->dims,
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,
xmb->alpha,
0,
0.5f,
&xmb_item_color[0],
xmb->shadow_offset / 2,
&mymat);
}
}
gfx_display_blend_end(dispctx, userdata);
gfx_display_blend_begin(dispctx, 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 = GFX_SURFACE_HANDLE(node->icon);
float x;
float y;
float scale_factor;
/* A tab draws the list's still, a playlist its own; one
* not landed, or an explore view, draws the cursor */
if (!texture)
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_info->dims,
shadows_enable,
xmb->icon_size,
xmb->icon_size,
texture,
x,
y,
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 (((video_info->menu.flags & VIDEO_MENU_FLAG_THUMBNAIL_BACKGROUND_ENABLE) ? true : false))
gfx_display_draw_quad(
p_disp,
userdata,
video_info->dims,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width, scaled_thumb_height),
video_info->dims,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_info->dims,
&xmb->thumbnails.savestate,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK(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_info->dims,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width, scaled_thumb_height),
video_info->dims,
background_color,
NULL);
if (!(xmb->thumbnails.savestate.flags & GFX_THUMB_FLAG_BG_ONLY))
xmb_draw_no_thumbnail_available(
xmb,
video_info,
p_disp,
userdata,
video_info->dims,
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
&& !xmb->media.view.settings_preview)
{
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 = ((video_info->menu.flags & VIDEO_MENU_FLAG_THUMBNAIL_BACKGROUND_ENABLE) ? true : false);
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_info->dims,
thumb_x,
right_thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width,
scaled_thumb_height),
video_info->dims,
background_color,
NULL);
gfx_display_draw_quad(
p_disp,
userdata,
video_info->dims,
thumb_x,
left_thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width,
scaled_thumb_height),
video_info->dims,
background_color,
NULL);
}
gfx_thumbnail_draw(
userdata,
video_info->dims,
&xmb->thumbnails.right,
thumb_x,
right_thumb_y,
VIDEO_SCALE_PACK((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_info->dims,
&xmb->thumbnails.left,
thumb_x,
left_thumb_y,
VIDEO_SCALE_PACK((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_info->dims,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width,
scaled_thumb_height),
video_info->dims,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_info->dims,
(show_right_thumbnail) ? &xmb->thumbnails.right : &xmb->thumbnails.left,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK((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_info->dims,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width,
scaled_thumb_height),
video_info->dims,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_info->dims,
&xmb->thumbnails.right,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK((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 = video_info->menu.icon_thumbnails;
uint8_t current_menu_icon = video_info->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_info->dims,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width,
scaled_thumb_height),
video_info->dims,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_info->dims,
&xmb->thumbnails.left,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK((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 = video_info->menu.icon_thumbnails;
uint8_t current_menu_icon = video_info->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_info->dims,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK(scaled_thumb_width, scaled_thumb_height),
video_info->dims,
background_color,
NULL);
gfx_thumbnail_draw(
userdata,
video_info->dims,
(show_left_thumbnail) ? &xmb->thumbnails.left : &xmb->thumbnails.right,
thumb_x,
thumb_y,
VIDEO_SCALE_PACK((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);
}
}
}
gfx_display_blend_end(dispctx, 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);
gfx_display_blend_begin(dispctx, userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_info->dims,
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,
xmb->alpha,
0,
scale_factor,
&xmb_item_color[0],
xmb->shadow_offset,
&mymat);
gfx_display_blend_end(dispctx, 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, video_info, msg,
video_width - xmb->margins_title_left - x_pos,
xmb->margins_title_top, 1, 1, TEXT_ALIGN_RIGHT,
video_info->dims, 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);
gfx_display_blend_begin(dispctx, userdata);
xmb_draw_icon(
userdata,
p_disp,
dispctx,
video_info->dims,
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,
xmb->alpha,
0,
scale_factor,
&xmb_item_color[0],
xmb->shadow_offset,
&mymat);
gfx_display_blend_end(dispctx, userdata);
}
datetime.time_mode = video_info->menu.timedate_style;
datetime.date_separator = video_info->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, video_info, 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_info->dims, 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
&& video_info->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 = ((video_info->menu.flags & VIDEO_MENU_FLAG_TICKER_SMOOTH) ? true : false);
enum gfx_animation_ticker_type menu_ticker_type
= (enum gfx_animation_ticker_type)video_info->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, video_info,
tmp,
(float)ticker_x_offset + xmb->margins_title_left + icon_len,
xmb->margins_title_top,
1, 1, TEXT_ALIGN_LEFT,
video_info->dims, xmb->font);
}
if (menu_core_enable)
{
menu_entries_get_core_title(title_msg, sizeof(title_msg));
xmb_draw_text(shadows_enable, xmb, video_info,
title_msg,
xmb->margins_title_left,
video_height - xmb->margins_title_bottom,
1, 1, TEXT_ALIGN_LEFT,
video_info->dims, 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_info->dims,
xmb->font->renderer_data);
if (xmb->font2 && xmb->font2->renderer && xmb->font2->renderer->flush)
xmb->font2->renderer->flush(video_info->dims,
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_info->dims,
shadows_enable,
color_theme,
xmb_item_color,
video_info, selection);
menu_media_controls_draw(&xmb->media, p_disp, userdata,
video_info->dims, xmb->font2 ? xmb->font2 : xmb->font);
if (xmb->media.view.settings_preview)
{
unsigned preview_width = video_width / 4;
unsigned preview_height = video_height / 2;
gfx_thumbnail_t *preview = xmb->thumbnails.right.status == GFX_THUMBNAIL_STATUS_AVAILABLE
? &xmb->thumbnails.right : &xmb->thumbnails.left;
gfx_thumbnail_draw(userdata, video_info->dims, preview,
(float)(video_width - preview_width), (float)(video_height / 4),
VIDEO_SCALE_PACK(preview_width, preview_height),
GFX_THUMBNAIL_ALIGN_CENTRE, 1.0f, 1.0f, NULL);
}
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_info->dims,
(input_dialog_display_kb) ? 0.95f : 0.75f);
if (xmb->font && *msg)
xmb_render_messagebox_internal(userdata, video_info, p_disp,
dispctx,
video_info->dims,
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_info->dims,
cursor_visible,
&coord_white[0],
xmb->cursor_size,
tex_list[XMB_TEXTURE_POINTER],
VIDEO_POS_X(xmb->pointer.pos),
VIDEO_POS_Y(xmb->pointer.pos));
}
if (video_st->current_video && video_st->current_video->set_viewport)
video_st->current_video->set_viewport(
video_st->data, video_info->dims, 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_init_ribbon(xmb_handle_t * xmb)
{
gfx_display_mesh_desc_t desc;
unsigned r, c, col;
unsigned i = 0;
unsigned vertices_total = XMB_RIBBON_VERTICES;
gfx_display_mesh_vertex_t *vertices = (gfx_display_mesh_vertex_t*)
calloc(vertices_total, sizeof(*vertices));
/* The ribbon is a decorative background; without memory for it the
* menu goes without */
if (!vertices)
return;
/* One strip over the 64x64 grid, row after row, each row running
* back the way the one before came. The ribbon programs read the
* position only. */
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;
vertices[i].x = ((float)col) / (XMB_RIBBON_COLS - 1) * 2.0f - 1.0f;
vertices[i].y = ((float)r) / (XMB_RIBBON_ROWS - 1) * 2.0f - 1.0f;
vertices[i + 1].x = ((float)col) / (XMB_RIBBON_COLS - 1) * 2.0f - 1.0f;
vertices[i + 1].y = ((float)(r + 1)) / (XMB_RIBBON_ROWS - 1) * 2.0f - 1.0f;
i += 2;
}
}
desc.vertices = vertices;
desc.indices = NULL;
desc.vertex_count = vertices_total;
desc.index_count = 0;
desc.topology = GFX_MESH_TRIANGLE_STRIP;
desc.flags = GFX_MESH_FLAG_POSITIONS;
xmb->ribbon_mesh = gfx_display_mesh_create(&desc);
free(vertices);
}
static void xmb_menu_animation_update_time(
float *ticker_pixel_increment,
unsigned video_dims)
{
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)
{
unsigned out_dims;
int i;
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;
out_dims = video_driver_get_output_dims();
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_dims = out_dims;
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_dims = out_dims;
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, out_dims);
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, VIDEO_SCALE_W(out_dims));
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;
}
/* The list's stills, and the tabs that draw them */
static void xmb_free_textures(xmb_handle_t *xmb)
{
unsigned i;
for (i = 0; i < XMB_TEXTURE_LAST; i++)
{
gfx_surface_free(xmb->textures.list[i]);
xmb->textures.list[i] = NULL;
}
xmb->main_menu_node.icon = NULL;
xmb->settings_tab_node.icon = NULL;
xmb->history_tab_node.icon = NULL;
xmb->favorites_tab_node.icon = NULL;
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
xmb->images_tab_node.icon = NULL;
#endif
xmb->music_tab_node.icon = NULL;
#if defined(HAVE_FFMPEG) || defined(HAVE_MPV)
xmb->video_tab_node.icon = NULL;
#endif
xmb->add_tab_node.icon = NULL;
xmb->contentless_cores_tab_node.icon = NULL;
#if defined(HAVE_LIBRETRODB)
xmb->explore_tab_node.icon = NULL;
#endif
#ifdef HAVE_NETWORKING
xmb->netplay_tab_node.icon = NULL;
#endif
}
static void xmb_free(void *data)
{
xmb_handle_t *xmb = (xmb_handle_t*)data;
if (xmb)
{
menu_media_controls_cancel(&xmb->media);
xmb_free_textures(xmb);
xmb_context_bg_destroy(xmb);
/* 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);
gfx_display_mesh_free(xmb->ribbon_mesh);
xmb->ribbon_mesh = NULL;
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);
}
}
static void xmb_context_bg_destroy(xmb_handle_t *xmb)
{
if (!xmb)
return;
gfx_surface_free(xmb->textures.bg);
xmb->textures.bg = NULL;
}
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:
/* Replaces the one up, once it is loaded */
gfx_surface_take_image(gfx_surface_still(&xmb->textures.bg,
gfx_display_texture_filter()),
(struct texture_image*)data);
break;
case MENU_IMAGE_NONE:
default:
xmb_context_bg_destroy(xmb);
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);
if (xmb->show_fullscreen_thumbnails
&& (xmb->is_file_list || (xmb->is_playlist
&& menu_media_selected_in_history(menu_state_get_ptr())))
&& menu_media_open_selected_file(menu_state_get_ptr(),
&xmb->thumbnails.left, &xmb->thumbnails.right))
xmb->thumbnails.pending = XMB_PENDING_THUMBNAIL_NONE;
}
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 = VIDEO_SCALE_H(xmb->last_dims);
/* 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, for as many tabs as the
* array holds */
if (xmb->categories_selection_ptr < ARRAY_SIZE(xmb->tab_selection))
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;
file_list_free_label(menu_stack, stack_size - 1);
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;
#if defined(HAVE_IMAGEVIEWER) || defined(HAVE_MENU)
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)
{
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. */
menu_media_controls_context_reset(&xmb->media, true);
xmb->context_generation++;
xmb_free_textures(xmb);
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;
if (xmb->show_fullscreen_thumbnails && xmb->media.controls.target.user)
{
gfx_media_controls_pointer(&xmb->media.controls, (int)x, (int)y,
true, true, menu_driver_get_current_time());
return 0;
}
/* 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 (xmb->show_fullscreen_thumbnails
&& menu_media_controls_release(&xmb->media, x, y,
menu_driver_get_current_time()))
{
menu_st->input_state.pointer.y_accel = 0.0f;
return 0;
}
/* 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 = VIDEO_SCALE_W(xmb->last_dims);
height = VIDEO_SCALE_H(xmb->last_dims);
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
};