FFmpeg coverage


Directory: ../../../ffmpeg/
File: src/fftools/ffplay_list.c
Date: 2026-10-19 14:07:01
Exec Total Coverage
Lines: 0 462 0.0%
Functions: 0 42 0.0%
Branches: 0 363 0.0%

Line Branch Exec Source
1 /*
2 * This file is part of FFmpeg.
3 *
4 * FFmpeg is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Lesser General Public
6 * License as published by the Free Software Foundation; either
7 * version 2.1 of the License, or (at your option) any later version.
8 *
9 * FFmpeg is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Lesser General Public License for more details.
13 *
14 * You should have received a copy of the GNU Lesser General Public
15 * License along with FFmpeg; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17 */
18
19 #include "config.h"
20
21 #include <limits.h>
22 #include <string.h>
23
24 #if CONFIG_LIBASS
25 #include <ass/ass.h>
26 #endif
27
28 #include "libavutil/avstring.h"
29 #include "libavutil/avutil.h"
30 #include "libavutil/bprint.h"
31 #include "libavutil/common.h"
32 #include "libavutil/error.h"
33 #include "libavutil/log.h"
34 #include "libavutil/mathematics.h"
35 #include "libavutil/mem.h"
36 #include "libavutil/qsort.h"
37 #include "libavutil/time.h"
38
39 #include "ffplay_list.h"
40
41 #define LIST_FADE_TIME 250000
42 #define LIST_HOLD_TIME 500000
43 #define LIST_ROWS 7
44
45 enum {
46 LIST_SORT_NUMBER,
47 LIST_SORT_ARTIST,
48 LIST_SORT_TITLE,
49 LIST_SORT_LENGTH,
50 LIST_SORT_SHUFFLE,
51 LIST_SORT_NB
52 };
53
54 typedef struct ListSortKey {
55 int key;
56 int descending;
57 } ListSortKey;
58
59 /* what lies under a window position: the row and the sort button, -1 for none, and whether it is inside the list at all */
60 typedef struct ListHit {
61 int inside;
62 int row;
63 int button;
64 int detail;
65 } ListHit;
66
67 /* a strip rendered once, kept while it may be shown again */
68 typedef struct ListCacheEntry {
69 char *script;
70 uint32_t background;
71 int x, w, h;
72 uint8_t *pixels;
73 unsigned last_used;
74 } ListCacheEntry;
75
76 /* a horizontal band of the list bitmap and the rendered strip shown there */
77 typedef struct ListStrip {
78 SDL_Rect rect;
79 ListCacheEntry *entry;
80 } ListStrip;
81
82 struct ListView {
83 SDL_Renderer *renderer;
84 SDL_Texture *texture;
85 SDL_Rect dirty;
86 uint8_t *bitmap;
87 SDL_Rect rect;
88 #if CONFIG_LIBASS
89 ASS_Library *library;
90 ASS_Renderer *ass;
91 ASS_Track *track;
92 #endif
93 ListStrip *strips;
94 int nb_strips;
95 ListCacheEntry **cache;
96 int nb_cache;
97 size_t cache_bytes;
98 unsigned frame;
99 ListEntry *entries;
100 int nb_entries;
101 /* whether any entry has an artist or a length, without which the column has no sort button */
102 int has_artist, has_length;
103 /* the listed entries in display order, and the scratch space sorting them needs */
104 int *rows, *sort_tmp;
105 int nb_rows;
106 char search[64];
107 int selected;
108 int playing;
109 int mouse_x, mouse_y;
110 ListHit hover;
111 int render_pending;
112 int changed;
113 int pinned;
114 /* the search line is shown and typed into: set while pinned, kept until the list is hidden */
115 int typing;
116 int64_t fade_start;
117 int64_t last_input;
118 double drawn_alpha;
119 int width, height;
120 /* the columns the list is sorted by, the most recently clicked first */
121 ListSortKey sort[LIST_SORT_NB];
122 int shuffle_seed;
123 int libass_missing_reported;
124 /* 16.16 reciprocals of the alpha values, for the un-premultiplication of blended pixels */
125 uint32_t reciprocal[256];
126 };
127
128 ✗ static double list_alpha(const ListView *lv, int64_t now)
129 {
130 ✗ double alpha = (now - lv->fade_start) / (double)LIST_FADE_TIME;
131
132 ✗ if (!lv->bitmap)
133 ✗ return 0;
134 ✗ if (!lv->pinned)
135 ✗ alpha = FFMIN(alpha, 1 - (now - lv->last_input - LIST_HOLD_TIME) / (double)LIST_FADE_TIME);
136 ✗ return av_clipd(alpha, 0, 1);
137 }
138
139 /* case-insensitive, with missing tags after all others */
140 ✗ static int compare_tags(const char *a, const char *b)
141 {
142 ✗ a = av_x_if_null(a, "");
143 ✗ b = av_x_if_null(b, "");
144 ✗ return !*a != !*b ? !*a - !*b : av_strcasecmp(a, b);
145 }
146
147 /* the rank of an entry in the shuffled order for the current seed */
148 ✗ static uint32_t shuffle_rank(const ListView *lv, int index)
149 {
150 ✗ uint32_t rank = (index + 1) * 0x9E3779B1u ^ lv->shuffle_seed * 0x85EBCA77u;
151
152 ✗ rank ^= rank >> 15;
153 ✗ rank *= 0x2C1B3C6Du;
154 ✗ return rank ^ rank >> 12;
155 }
156
157 ✗ static int compare_rows(const ListView *lv, const int *a, const int *b)
158 {
159 ✗ const ListEntry *ea = &lv->entries[*a], *eb = &lv->entries[*b];
160
161 ✗ for (int i = 0; i < LIST_SORT_NB; i++) {
162 int cmp;
163
164 ✗ switch (lv->sort[i].key) {
165 ✗ case LIST_SORT_ARTIST: cmp = compare_tags(ea->artist, eb->artist); break;
166 ✗ case LIST_SORT_TITLE: cmp = compare_tags(ea->title, eb->title); break;
167 ✗ case LIST_SORT_LENGTH: cmp = FFDIFFSIGN(ea->length, eb->length); break;
168 ✗ case LIST_SORT_SHUFFLE: cmp = FFDIFFSIGN(shuffle_rank(lv, *a), shuffle_rank(lv, *b)); break;
169 ✗ default: cmp = *a - *b; break;
170 }
171 ✗ if (cmp)
172 ✗ return lv->sort[i].descending ? -cmp : cmp;
173 }
174 ✗ return 0;
175 }
176
177 /* a column clicked again flips its order, any other becomes the first key ahead of the previous ones;
178 * the shuffle button instead steps its seed by the side it was clicked on */
179 ✗ static void list_sort_by(ListView *lv, int key, int seed_step)
180 {
181 ✗ int i = 0;
182
183 ✗ while (lv->sort[i].key != key)
184 ✗ i++;
185 ✗ if (key == LIST_SORT_SHUFFLE)
186 ✗ lv->shuffle_seed += seed_step;
187 ✗ else if (i == 0)
188 ✗ lv->sort[0].descending ^= 1;
189 ✗ if (i == 0)
190 ✗ return;
191 ✗ memmove(&lv->sort[1], &lv->sort[0], i * sizeof(*lv->sort));
192 ✗ lv->sort[0] = (ListSortKey){ key, 0 };
193 }
194
195 /* rebuilds the rows matching the search in the current sort order, keeping the selected entry selected */
196 ✗ static void list_update_rows(ListView *lv)
197 {
198 ✗ int selected = list_view_selected(lv);
199
200 ✗ lv->nb_rows = 0;
201 ✗ for (int i = 0; i < lv->nb_entries; i++)
202 ✗ if (av_stristr(av_x_if_null(lv->entries[i].artist, ""), lv->search) ||
203 ✗ av_stristr(av_x_if_null(lv->entries[i].title, ""), lv->search))
204 ✗ lv->rows[lv->nb_rows++] = i;
205 #define CMP(a, b) compare_rows(lv, a, b)
206 ✗ AV_MSORT(lv->rows, lv->sort_tmp, lv->nb_rows, int, CMP);
207 #undef CMP
208 ✗ lv->selected = 0;
209 ✗ for (int row = 0; row < lv->nb_rows; row++)
210 ✗ if (lv->rows[row] == selected)
211 ✗ lv->selected = row;
212 ✗ }
213
214 static const struct {
215 const char *label;
216 int width;
217 } sort_buttons[LIST_SORT_NB] = {
218 [LIST_SORT_NUMBER] = { "#", 5 },
219 [LIST_SORT_ARTIST] = { "Artist", 8 },
220 [LIST_SORT_TITLE] = { "Title", 7 },
221 [LIST_SORT_LENGTH] = { "Duration", 10 },
222 [LIST_SORT_SHUFFLE] = { "Shuffle", 14 },
223 };
224
225 #define LIST_EVENT "Dialogue: 0:00:00.00,9999:00:00.00,"
226
227 ✗ static void bprint_box(AVBPrint *script, const char *style, int x, int y, int w, int h)
228 {
229 ✗ av_bprintf(script, LIST_EVENT "%s,{\\pos(%d,%d)\\p1}m 0 0 l %d 0 %d %d 0 %d{\\p0}\n", style, x, y, w, w, h, h);
230 ✗ }
231
232 /* a text cell cut off at the right and bottom of its column, with the characters libass would read as tags or line breaks blanked */
233 ✗ static void bprint_cell(AVBPrint *script, const char *style, int x, int y, int right, int bottom, const char *text)
234 {
235 ✗ av_bprintf(script, LIST_EVENT "%s,{\\pos(%d,%d)\\clip(0,0,%d,%d)}", style, x, y, right, bottom);
236 ✗ for (text = av_x_if_null(text, ""); *text; text++)
237 ✗ av_bprint_chars(script, strchr("{}\\\r\n", *text) ? ' ' : *text, 1);
238 ✗ av_bprint_chars(script, '\n', 1);
239 ✗ }
240
241 typedef struct ListLayout {
242 int font, line, width, height, canvas_h, nb_rows, first;
243 int button_x[LIST_SORT_NB], button_y[LIST_SORT_NB];
244 int search_x, search_y, rows_y, text_x, text_right, length_x, detail_x, canvas_w;
245 } ListLayout;
246
247 /* button widths are in half font sizes, a quarter apart, wrapping onto further lines when the panel is too narrow */
248 ✗ static ListLayout list_layout(const ListView *lv)
249 {
250 ListLayout l;
251 int x, y;
252
253 ✗ l.font = FFMAX(lv->height / 30, 12);
254 ✗ l.line = l.font * 9 / 4;
255 ✗ l.width = FFMIN(FFMAX(lv->width * 3 / 5, l.font * 26), lv->width - l.font * 2);
256 ✗ x = y = l.font / 2;
257 ✗ for (int b = 0; b < LIST_SORT_NB; b++) {
258 ✗ int w = sort_buttons[b].width * l.font / 2;
259
260 ✗ if (b == LIST_SORT_ARTIST && !lv->has_artist || b == LIST_SORT_LENGTH && !lv->has_length) {
261 ✗ l.button_x[b] = -1;
262 ✗ l.button_y[b] = 0;
263 ✗ continue;
264 }
265 ✗ if (x > l.font / 2 && x + w > l.width - l.font / 2) {
266 ✗ x = l.font / 2;
267 ✗ y += l.font * 3 / 2;
268 }
269 ✗ l.button_x[b] = x;
270 ✗ l.button_y[b] = y;
271 ✗ x += w + l.font / 4;
272 }
273 /* the search field follows the buttons, or takes its own line when they leave it less than 8 em */
274 ✗ if (x + l.font * 8 > l.width - l.font / 2) {
275 ✗ x = l.font / 2;
276 ✗ y += l.font * 3 / 2;
277 }
278 ✗ l.search_x = x;
279 ✗ l.search_y = y;
280 ✗ l.rows_y = lv->typing ? y + l.font * 3 / 2 : l.button_y[LIST_SORT_NB - 1] + l.font * 3 / 2;
281 ✗ l.canvas_h = FFMIN(y + l.font * 2 + LIST_ROWS * l.line, lv->height - l.font * 2);
282 ✗ l.nb_rows = FFMIN(lv->nb_rows, FFMAX(1, (l.canvas_h - l.rows_y - l.font / 2) / l.line));
283 ✗ l.first = av_clip(lv->selected - l.nb_rows / 2, 0, lv->nb_rows - l.nb_rows);
284 ✗ l.height = l.rows_y + l.nb_rows * l.line + l.font / 2;
285 ✗ l.text_x = l.font * 3;
286 ✗ l.text_right = l.width - l.font * 11 / 2;
287 ✗ l.length_x = l.width - l.font * 5 / 2;
288 ✗ l.detail_x = l.width - l.font * 5 / 4;
289 ✗ l.canvas_w = lv->width - l.font * 2;
290 ✗ return l;
291 }
292
293 ✗ static ListHit list_hit(const ListLayout *l, int x, int y)
294 {
295 ✗ ListHit hit = { 0, -1, -1, 0 };
296 int line;
297
298 ✗ x -= l->font;
299 ✗ y -= l->font;
300 ✗ if (x < 0 || y < 0 || x >= l->width || y >= FFMIN(l->height, l->canvas_h))
301 ✗ return hit;
302 ✗ hit.inside = 1;
303 ✗ line = (y - l->rows_y) / l->line;
304 ✗ if (y >= l->rows_y && line < l->nb_rows) {
305 ✗ hit.row = l->first + line;
306 ✗ hit.detail = x >= l->width - l->font * 2;
307 }
308 ✗ for (int b = 0; b < FF_ARRAY_ELEMS(sort_buttons); b++)
309 ✗ if (l->button_x[b] >= 0 && x >= l->button_x[b] && x < l->button_x[b] + sort_buttons[b].width * l->font / 2 &&
310 ✗ y >= l->button_y[b] && y < l->button_y[b] + l->font * 5 / 4)
311 ✗ hit.button = b;
312 ✗ return hit;
313 }
314
315 /* a circle outline drawn as four bezier arcs around a center */
316 ✗ static void bprint_ring(AVBPrint *script, int cx, int cy, int r)
317 {
318 ✗ int k = r * 552 / 1000;
319
320 ✗ av_bprintf(script, LIST_EVENT "Ring,{\\an5\\pos(%d,%d)\\p1}m 0 %d b 0 %d %d 0 %d 0 b %d 0 %d %d %d %d "
321 "b %d %d %d %d %d %d b %d %d 0 %d 0 %d{\\p0}\n",
322 cx, cy, r, r - k, r - k, r, r + k, 2 * r, r - k, 2 * r, r, 2 * r, r + k, r + k, 2 * r, r, 2 * r,
323 r - k, 2 * r, r + k, r);
324 ✗ }
325
326 /* strip backgrounds as ARGB */
327 #define PANEL_COLOR 0xBF000000
328 #define HOVER_COLOR 0xCD444444
329 #define POPUP_COLOR 0xDF000000
330
331 ✗ static void bprint_styles(AVBPrint *script, const ListLayout *l)
332 {
333 ✗ av_bprintf(script,
334 "[Script Info]\nScriptType: v4.00+\nPlayResX: %d\nPlayResY: %d\nWrapStyle: 2\nYCbCr Matrix: None\n\n"
335 "[V4+ Styles]\n"
336 "Format: Name, Fontname, Fontsize, PrimaryColour, OutlineColour, Bold, Outline, Alignment\n"
337 "Style: Button,Sans,%d,&H80FFFFFF,&H80FFFFFF,0,0,7\n"
338 "Style: Row,Sans,%d,&H00FFFFFF,&H00000000,0,%d,7\n"
339 "Style: Selected,Sans,%d,&H0080D0FF,&H00000000,-1,%d,7\n"
340 "Style: Artist,Sans,%d,&H00C0C0C0,&H00000000,0,%d,7\n"
341 "Style: ArtistSelected,Sans,%d,&H0080D0FF,&H00000000,-1,%d,7\n"
342 "Style: Playing,Sans,%d,&H00FFFFFF,&H00FFFFFF,0,0,7\n"
343 "Style: Hint,Sans,%d,&H00A0A0A0,&H00000000,0,%d,7\n"
344 "Style: ButtonHover,Sans,%d,&H50FFFFFF,&H50FFFFFF,0,0,7\n"
345 "Style: Ring,Sans,%d,&HFF000000,&H00A0A0A0,0,%d,7\n"
346 "Style: Detail,Sans,%d,&H00FFFFFF,&H00000000,0,%d,7\n\n"
347 "[Events]\nFormat: Start, End, Style, Text\n",
348 ✗ l->canvas_w, l->canvas_h, l->font, l->font, l->font / 16 + 1, l->font, l->font / 16 + 1,
349 ✗ l->font * 4 / 5, l->font / 16 + 1, l->font * 4 / 5, l->font / 16 + 1, l->font, l->font, l->font / 16 + 1,
350 ✗ l->font, l->font, l->font / 16 + 1, l->font * 4 / 5, l->font / 16 + 1);
351 ✗ }
352
353 ✗ static void bprint_header_strip(const ListView *lv, AVBPrint *script, const ListLayout *l)
354 {
355 ✗ if (lv->typing)
356 ✗ bprint_cell(script, *lv->search ? "Row" : "Hint", l->search_x, l->search_y, l->length_x,
357 ✗ l->search_y + l->font * 3 / 2, *lv->search ? lv->search : "type to search");
358 ✗ for (int b = 0; b < FF_ARRAY_ELEMS(sort_buttons); b++) {
359 ✗ int x = l->button_x[b], y = l->button_y[b], w = sort_buttons[b].width * l->font / 2;
360
361 ✗ if (x < 0)
362 ✗ continue;
363 ✗ bprint_box(script, b == lv->hover.button ? "ButtonHover" : "Button", x, y, w, l->font * 5 / 4);
364 ✗ av_bprintf(script, LIST_EVENT "%s,{\\an5\\pos(%d,%d)}", lv->sort[0].key == b ? "Selected" : "Row",
365 ✗ x + w / 2, y + l->font * 5 / 8);
366 ✗ if (b == LIST_SORT_SHUFFLE)
367 ✗ av_bprintf(script, "\xe2\x97\x82 %s %d \xe2\x96\xb8\n", sort_buttons[b].label, lv->shuffle_seed);
368 else
369 ✗ av_bprintf(script, "%s%s\n", sort_buttons[b].label,
370 ✗ lv->sort[0].key != b ? "" : lv->sort[0].descending ? " \xe2\x96\xbc" : " \xe2\x96\xb2");
371 }
372 ✗ }
373
374 ✗ static void bprint_row_strip(const ListView *lv, AVBPrint *script, const ListLayout *l, int r)
375 {
376 ✗ int index = lv->rows[r];
377 ✗ const ListEntry *entry = &lv->entries[index];
378 ✗ const char *style = r == lv->selected ? "Selected" : "Row";
379 int64_t seconds;
380
381 ✗ if (index == lv->playing)
382 ✗ bprint_box(script, "Playing", 0, 0, l->font / 6, l->line);
383 ✗ av_bprintf(script, LIST_EVENT "%s,{\\an6\\pos(%d,%d)}%d\n", style, l->font * 5 / 2, l->line / 2, index + 1);
384 ✗ bprint_cell(script, style, l->text_x, l->font / 8, l->text_right, l->line, entry->title);
385 ✗ bprint_cell(script, r == lv->selected ? "ArtistSelected" : "Artist", l->text_x, l->font * 5 / 4,
386 ✗ l->text_right, l->line, entry->artist);
387 ✗ bprint_ring(script, l->detail_x, l->line / 2, l->font * 9 / 20);
388 ✗ av_bprintf(script, LIST_EVENT "Hint,{\\an5\\pos(%d,%d)}i\n", l->detail_x, l->line / 2);
389 ✗ if (entry->length < 0)
390 ✗ return;
391 ✗ seconds = av_rescale_rnd(entry->length, 1, AV_TIME_BASE, AV_ROUND_NEAR_INF);
392 ✗ av_bprintf(script, LIST_EVENT "%s,{\\an6\\pos(%d,%d)}", style, l->length_x, l->line / 2);
393 ✗ if (seconds >= 3600)
394 ✗ av_bprintf(script, "%"PRId64":%02d:%02d\n", seconds / 3600, (int)(seconds / 60 % 60), (int)(seconds % 60));
395 else
396 ✗ av_bprintf(script, "%d:%02d\n", (int)(seconds / 60), (int)(seconds % 60));
397 }
398
399 ✗ static void cache_entry_free(ListCacheEntry **entry)
400 {
401 ✗ if (*entry) {
402 ✗ av_freep(&(*entry)->script);
403 ✗ av_freep(&(*entry)->pixels);
404 }
405 ✗ av_freep(entry);
406 ✗ }
407
408 /* drops the least recently used strips not shown in this frame until the new one fits into twice the bitmap size */
409 ✗ static void cache_make_room(ListView *lv, size_t bytes)
410 {
411 ✗ while (lv->cache_bytes + bytes > 2 * (size_t)lv->rect.w * lv->rect.h * 4) {
412 ✗ int oldest = -1;
413
414 ✗ for (int i = 0; i < lv->nb_cache; i++)
415 ✗ if (lv->cache[i]->last_used != lv->frame && (oldest < 0 || lv->cache[i]->last_used < lv->cache[oldest]->last_used))
416 ✗ oldest = i;
417 ✗ if (oldest < 0)
418 ✗ return;
419 ✗ lv->cache_bytes -= (size_t)lv->cache[oldest]->w * lv->cache[oldest]->h * 4;
420 ✗ cache_entry_free(&lv->cache[oldest]);
421 ✗ lv->cache[oldest] = lv->cache[--lv->nb_cache];
422 }
423 }
424
425 static int strip_render(ListView *lv, ListCacheEntry *entry);
426
427 /* the cached strip for the script, background and place, rendered when there is none */
428 ✗ static int cache_get(ListView *lv, const SDL_Rect *rect, uint32_t background, const AVBPrint *script, ListCacheEntry **entry)
429 {
430 int ret;
431
432 ✗ for (int i = 0; i < lv->nb_cache; i++) {
433 ✗ ListCacheEntry *e = lv->cache[i];
434
435 ✗ if (e->background == background && e->x == rect->x && e->w == rect->w && e->h == rect->h &&
436 ✗ !strcmp(e->script, script->str)) {
437 ✗ e->last_used = lv->frame;
438 ✗ *entry = e;
439 ✗ return 0;
440 }
441 }
442 ✗ cache_make_room(lv, (size_t)rect->w * rect->h * 4);
443 ✗ if (!(*entry = av_mallocz(sizeof(**entry))))
444 ✗ return AVERROR(ENOMEM);
445 ✗ **entry = (ListCacheEntry){ av_strdup(script->str), background, rect->x, rect->w, rect->h,
446 ✗ av_malloc_array(rect->w * rect->h, 4), lv->frame };
447 ✗ ret = (*entry)->script && (*entry)->pixels ? strip_render(lv, *entry) : AVERROR(ENOMEM);
448 ✗ if (ret >= 0)
449 ✗ ret = av_dynarray_add_nofree(&lv->cache, &lv->nb_cache, *entry);
450 ✗ if (ret < 0) {
451 ✗ cache_entry_free(entry);
452 ✗ return ret;
453 }
454 ✗ lv->cache_bytes += (size_t)rect->w * rect->h * 4;
455 ✗ return 0;
456 }
457
458 ✗ static int strip_add(ListView *lv, ListStrip **strips, int *nb, SDL_Rect rect, uint32_t background, const AVBPrint *script)
459 {
460 ListCacheEntry *entry;
461 ListStrip *strip;
462 int ret;
463
464 ✗ if (rect.w <= 0 || rect.h <= 0)
465 ✗ return 0;
466 ✗ if (!av_bprint_is_complete(script))
467 ✗ return AVERROR(ENOMEM);
468 ✗ if ((ret = cache_get(lv, &rect, background, script, &entry)) < 0)
469 ✗ return ret;
470 ✗ if (!(strip = av_dynarray2_add((void **)strips, nb, sizeof(*strip), NULL)))
471 ✗ return AVERROR(ENOMEM);
472 ✗ *strip = (ListStrip){ rect, entry };
473 ✗ return 0;
474 }
475
476 /* the strips the list consists of right now: the header, each visible row, the bottom padding and the metadata popup,
477 * beside the list or over its rows when the window leaves no room there */
478 ✗ static int list_strips(ListView *lv, const ListLayout *l, ListStrip **strips, int *nb)
479 {
480 AVBPrint script;
481 ✗ int ret = 0;
482
483 ✗ av_bprint_init(&script, 0, AV_BPRINT_SIZE_UNLIMITED);
484 ✗ for (int i = 0; ret >= 0 && i < l->nb_rows + 2; i++) {
485 ✗ int row = i && i <= l->nb_rows ? l->first + i - 1 : -1;
486 ✗ SDL_Rect rect = { 0, i ? l->rows_y + (i - 1) * l->line : 0, l->width, i ? l->line : l->rows_y };
487
488 ✗ if (i > l->nb_rows)
489 ✗ rect.h = l->font / 2;
490 ✗ rect.h = FFMIN(rect.h, l->canvas_h - rect.y);
491 ✗ av_bprint_clear(&script);
492 ✗ if (!i)
493 ✗ bprint_header_strip(lv, &script, l);
494 ✗ else if (row >= 0)
495 ✗ bprint_row_strip(lv, &script, l, row);
496 ✗ ret = strip_add(lv, strips, nb, rect, row >= 0 && row == lv->hover.row ? HOVER_COLOR : PANEL_COLOR, &script);
497 }
498 ✗ if (ret >= 0 && lv->hover.detail) {
499 ✗ const AVDictionary *metadata = lv->entries[lv->rows[lv->hover.row]].metadata;
500 ✗ const AVDictionaryEntry *tag = NULL;
501 ✗ int beside = l->canvas_w - l->width >= l->font * 12;
502 ✗ int height = FFMIN(av_dict_count(metadata) * l->font + l->font / 2, l->canvas_h);
503 ✗ int x = beside ? l->width + l->font / 2 : l->text_x;
504 ✗ int right = beside ? l->canvas_w : l->length_x - l->font / 2;
505 ✗ SDL_Rect rect = { x, av_clip(l->rows_y + (lv->hover.row - l->first) * l->line, 0, l->canvas_h - height),
506 ✗ right - x, height };
507
508 ✗ av_bprint_clear(&script);
509 ✗ for (int top = l->font / 4; top < height + l->font && (tag = av_dict_iterate(metadata, tag)); top += l->font) {
510 ✗ bprint_cell(&script, "Detail", x + l->font / 2, top, x + l->font * 7, height, tag->key);
511 ✗ bprint_cell(&script, "Detail", x + l->font * 15 / 2, top, right - l->font / 4, height, tag->value);
512 }
513 ✗ ret = strip_add(lv, strips, nb, rect, POPUP_COLOR, &script);
514 }
515 ✗ av_bprint_finalize(&script, NULL);
516 ✗ return ret;
517 }
518
519 ✗ static void list_discard(ListView *lv)
520 {
521 ✗ av_freep(&lv->bitmap);
522 ✗ av_freep(&lv->strips);
523 ✗ lv->nb_strips = 0;
524 ✗ for (int i = 0; i < lv->nb_cache; i++)
525 ✗ cache_entry_free(&lv->cache[i]);
526 ✗ av_freep(&lv->cache);
527 ✗ lv->nb_cache = 0;
528 ✗ lv->cache_bytes = 0;
529 ✗ if (lv->texture)
530 ✗ SDL_DestroyTexture(lv->texture);
531 ✗ lv->texture = NULL;
532 #if CONFIG_LIBASS
533 if (lv->track)
534 ass_free_track(lv->track);
535 lv->track = NULL;
536 #endif
537 ✗ }
538
539 #if CONFIG_LIBASS
540 static void libass_log(int libass_level, const char *fmt, va_list args, void *opaque)
541 {
542 static const int levels[] = { AV_LOG_FATAL, AV_LOG_ERROR, AV_LOG_WARNING, AV_LOG_WARNING,
543 AV_LOG_VERBOSE, AV_LOG_VERBOSE, AV_LOG_DEBUG, AV_LOG_TRACE };
544 int level = levels[av_clip(libass_level, 0, FF_ARRAY_ELEMS(levels) - 1)];
545
546 av_vlog(NULL, level, fmt, args);
547 av_log(NULL, level, "\n");
548 }
549 #endif
550
551 /* the bitmap, the texture it is uploaded to, and libass with the styles of the layout, rendering a strip at a time */
552 ✗ static int list_configure(ListView *lv, const ListLayout *l, const SDL_Rect *canvas)
553 {
554 #if CONFIG_LIBASS
555 AVBPrint styles;
556
557 list_discard(lv);
558 lv->rect = *canvas;
559 if (l->canvas_w <= 0 || l->canvas_h <= 0 || l->canvas_w > INT_MAX / 4 / l->canvas_h)
560 return AVERROR(EINVAL);
561 if (!(lv->bitmap = av_mallocz(l->canvas_w * l->canvas_h * 4)))
562 return AVERROR(ENOMEM);
563 if (!(lv->texture = SDL_CreateTexture(lv->renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING,
564 l->canvas_w, l->canvas_h)) ||
565 SDL_SetTextureBlendMode(lv->texture, SDL_BLENDMODE_BLEND) < 0)
566 return AVERROR_EXTERNAL;
567 lv->dirty = (SDL_Rect){ 0, 0, l->canvas_w, l->canvas_h };
568 if (!lv->library) {
569 if (!(lv->library = ass_library_init()))
570 return AVERROR(ENOMEM);
571 ass_set_message_cb(lv->library, libass_log, NULL);
572 ass_set_extract_fonts(lv->library, 1);
573 if (!(lv->ass = ass_renderer_init(lv->library))) {
574 ass_library_done(lv->library);
575 lv->library = NULL;
576 return AVERROR(ENOMEM);
577 }
578 ass_set_fonts(lv->ass, NULL, NULL, 1, NULL, 1);
579 }
580 av_bprint_init(&styles, 0, AV_BPRINT_SIZE_UNLIMITED);
581 bprint_styles(&styles, l);
582 if (av_bprint_is_complete(&styles) && (lv->track = ass_new_track(lv->library)))
583 ass_process_codec_private(lv->track, styles.str, styles.len);
584 av_bprint_finalize(&styles, NULL);
585 if (!lv->track)
586 return AVERROR(ENOMEM);
587 ass_set_frame_size(lv->ass, l->canvas_w, l->canvas_h);
588 ass_set_storage_size(lv->ass, l->canvas_w, l->canvas_h);
589 return 0;
590 #else
591 ✗ if (!lv->libass_missing_reported++)
592 ✗ av_log(NULL, AV_LOG_ERROR, "The list needs libass, which this build lacks\n");
593 ✗ return AVERROR(ENOSYS);
594 #endif
595 }
596
597 #if CONFIG_LIBASS
598 /* the color with opacity a over the pixel, both ARGB, using the 16.16 reciprocals of the alpha values */
599 static uint32_t blend_over(uint32_t dst, uint32_t rgb, int a, const uint32_t *reciprocal)
600 {
601 int dst_a = dst >> 24, out_a = a + (dst_a * (255 - a) + 127) / 255;
602 uint32_t out = (uint32_t)out_a << 24;
603
604 for (int shift = 0; shift < 24; shift += 8) {
605 int premultiplied = ((rgb >> shift & 0xFF) * a * 255 + (dst >> shift & 0xFF) * dst_a * (255 - a) + 255 * 127) / (255 * 255);
606
607 out |= FFMIN(premultiplied * reciprocal[out_a] >> 16, 255) << shift;
608 }
609 return out;
610 }
611 #endif
612
613 /* renders the strip over its background */
614 ✗ static int strip_render(ListView *lv, ListCacheEntry *entry)
615 {
616 #if CONFIG_LIBASS
617 uint32_t *pixels = (uint32_t *)entry->pixels;
618 int w = entry->w, h = entry->h;
619
620 ass_flush_events(lv->track);
621 ass_process_data(lv->track, entry->script, strlen(entry->script));
622 for (int i = 0; i < w * h; i++)
623 pixels[i] = entry->background;
624 for (const ASS_Image *image = ass_render_frame(lv->ass, lv->track, 0, NULL); image; image = image->next) {
625 int opacity = 255 - (image->color & 0xFF);
626 int x0 = FFMAX(image->dst_x, entry->x), x1 = FFMIN(image->dst_x + image->w, entry->x + w);
627
628 for (int y = FFMAX(image->dst_y, 0); x0 < x1 && y < FFMIN(image->dst_y + image->h, h); y++) {
629 const uint8_t *coverage = image->bitmap + (y - image->dst_y) * image->stride + x0 - image->dst_x;
630 uint32_t *dst = pixels + y * w + x0 - entry->x;
631
632 for (int x = 0; x < x1 - x0; x++) {
633 int a = (opacity * coverage[x] + 127) / 255;
634
635 if (a)
636 dst[x] = blend_over(dst[x], image->color >> 8, a, lv->reciprocal);
637 }
638 }
639 }
640 return 0;
641 #else
642 ✗ return AVERROR(ENOSYS);
643 #endif
644 }
645
646 /* brings the bitmap up to date: what differs from the strips shown before is cleared and composed again from the strips
647 * covering it, rendering only strips that are not cached */
648 ✗ static int list_render(ListView *lv)
649 {
650 ✗ ListLayout l = list_layout(lv);
651 ✗ SDL_Rect canvas = { l.font, l.font, l.canvas_w, l.canvas_h }, dirty = { 0, 0, 0, 0 };
652 ✗ ListStrip *strips = NULL;
653 ✗ int nb = 0, ret = 0;
654
655 ✗ lv->render_pending = 0;
656 ✗ lv->hover = list_hit(&l, lv->mouse_x, lv->mouse_y);
657 /* the strips shown stay cached until compared with the new ones */
658 ✗ lv->frame++;
659 ✗ for (int i = 0; i < lv->nb_strips; i++)
660 ✗ lv->strips[i].entry->last_used = lv->frame;
661 ✗ if (!lv->bitmap || !SDL_RectEquals(&lv->rect, &canvas))
662 ✗ ret = list_configure(lv, &l, &canvas);
663 ✗ if (ret >= 0)
664 ✗ ret = list_strips(lv, &l, &strips, &nb);
665 ✗ for (int i = 0; ret >= 0 && i < FFMAX(nb, lv->nb_strips); i++) {
666 ✗ const ListStrip *old = i < lv->nb_strips ? &lv->strips[i] : NULL, *new = i < nb ? &strips[i] : NULL;
667
668 ✗ if (old && new && old->entry == new->entry && SDL_RectEquals(&old->rect, &new->rect))
669 ✗ continue;
670 ✗ if (old)
671 ✗ SDL_UnionRect(&dirty, &old->rect, &dirty);
672 ✗ if (new)
673 ✗ SDL_UnionRect(&dirty, &new->rect, &dirty);
674 }
675 ✗ if (ret >= 0 && dirty.w) {
676 ✗ for (int y = dirty.y; y < dirty.y + dirty.h; y++)
677 ✗ memset(lv->bitmap + (y * lv->rect.w + dirty.x) * 4, 0, dirty.w * 4);
678 ✗ for (int i = 0; i < nb; i++) {
679 ✗ const ListCacheEntry *entry = strips[i].entry;
680 SDL_Rect part;
681
682 ✗ if (!SDL_IntersectRect(&strips[i].rect, &dirty, &part))
683 ✗ continue;
684 ✗ for (int y = part.y; y < part.y + part.h; y++)
685 ✗ memcpy(lv->bitmap + (y * lv->rect.w + part.x) * 4,
686 ✗ entry->pixels + ((y - strips[i].rect.y) * entry->w + part.x - strips[i].rect.x) * 4, part.w * 4);
687 }
688 ✗ SDL_UnionRect(&lv->dirty, &dirty, &lv->dirty);
689 ✗ lv->changed = 1;
690 }
691 ✗ if (ret < 0) {
692 ✗ if (ret != AVERROR(ENOSYS))
693 ✗ av_log(NULL, AV_LOG_ERROR, "Could not render the list: %s\n", av_err2str(ret));
694 ✗ list_discard(lv);
695 ✗ av_freep(&strips);
696 ✗ nb = 0;
697 ✗ if (lv->pinned)
698 ✗ SDL_StopTextInput();
699 ✗ lv->pinned = 0;
700 }
701 ✗ av_freep(&lv->strips);
702 ✗ lv->strips = strips;
703 ✗ lv->nb_strips = nb;
704 ✗ return ret;
705 }
706
707 ✗ static void list_select(ListView *lv, int row)
708 {
709 ✗ int64_t now = av_gettime_relative();
710
711 ✗ lv->selected = row;
712 ✗ lv->fade_start = now - list_alpha(lv, now) * LIST_FADE_TIME;
713 ✗ lv->last_input = now;
714 ✗ lv->render_pending = 1;
715 ✗ }
716
717 ✗ void list_view_text(ListView *lv, const char *text)
718 {
719 ✗ int len = strlen(lv->search);
720
721 ✗ if (text) {
722 ✗ if (len + strlen(text) < sizeof(lv->search))
723 ✗ av_strlcat(lv->search, text, sizeof(lv->search));
724 ✗ } else if (len) {
725 ✗ while (len > 0 && (lv->search[--len] & 0xC0) == 0x80)
726 ;
727 ✗ lv->search[len] = 0;
728 }
729 ✗ list_update_rows(lv);
730 ✗ lv->render_pending = 1;
731 ✗ }
732
733 ✗ ListView *list_view_alloc(SDL_Renderer *renderer)
734 {
735 ✗ ListView *lv = av_mallocz(sizeof(*lv));
736
737 ✗ if (!lv)
738 ✗ return NULL;
739 ✗ lv->renderer = renderer;
740 ✗ lv->playing = -1;
741 ✗ lv->mouse_x = lv->mouse_y = -1;
742 ✗ lv->hover = (ListHit){ 0, -1, -1, 0 };
743 ✗ lv->reciprocal[0] = 1 << 16;
744 ✗ for (int a = 1; a < 256; a++)
745 ✗ lv->reciprocal[a] = ((255 << 16) + a - 1) / a;
746 ✗ list_view_clear(lv);
747 ✗ return lv;
748 }
749
750 ✗ void list_view_free(ListView **plv)
751 {
752 ✗ ListView *lv = *plv;
753
754 ✗ if (!lv)
755 ✗ return;
756 ✗ list_discard(lv);
757 #if CONFIG_LIBASS
758 if (lv->ass)
759 ass_renderer_done(lv->ass);
760 if (lv->library)
761 ass_library_done(lv->library);
762 #endif
763 ✗ av_freep(&lv->entries);
764 ✗ av_freep(plv);
765 }
766
767 ✗ int list_view_show(ListView *lv, const ListEntry *entries, int nb_entries, int selected)
768 {
769 ListEntry *copy;
770 ✗ int previous = list_view_selected(lv);
771
772 ✗ if (!lv->renderer)
773 ✗ return AVERROR(EINVAL);
774 /* the rows and the sort scratch follow the entries */
775 ✗ copy = av_malloc_array(nb_entries, sizeof(*copy) + 2 * sizeof(*lv->rows));
776 ✗ if (!copy)
777 ✗ return AVERROR(ENOMEM);
778 ✗ if (nb_entries)
779 ✗ memcpy(copy, entries, nb_entries * sizeof(*entries));
780 ✗ av_free(lv->entries);
781 ✗ lv->entries = copy;
782 ✗ lv->rows = (int *)(copy + nb_entries);
783 ✗ lv->sort_tmp = lv->rows + nb_entries;
784 ✗ lv->nb_entries = nb_entries;
785 ✗ lv->nb_rows = 0;
786 ✗ lv->has_artist = lv->has_length = 0;
787 ✗ for (int i = 0; i < nb_entries; i++) {
788 ✗ lv->has_artist |= entries[i].artist && *entries[i].artist;
789 ✗ lv->has_length |= entries[i].length >= 0;
790 }
791 ✗ list_update_rows(lv);
792 ✗ for (int row = 0; row < lv->nb_rows; row++)
793 ✗ if (lv->rows[row] == previous)
794 ✗ lv->selected = row;
795 ✗ for (int row = 0; row < lv->nb_rows; row++)
796 ✗ if (lv->rows[row] == selected)
797 ✗ lv->selected = row;
798 ✗ list_select(lv, lv->selected);
799 ✗ return 0;
800 }
801
802 ✗ void list_view_clear(ListView *lv)
803 {
804 ✗ lv->nb_entries = lv->nb_rows = lv->selected = 0;
805 ✗ lv->search[0] = 0;
806 ✗ for (int i = 0; i < LIST_SORT_NB; i++)
807 ✗ lv->sort[i] = (ListSortKey){ i, 0 };
808 ✗ lv->shuffle_seed = 0;
809 ✗ list_discard(lv);
810 ✗ }
811
812 ✗ void list_view_pin(ListView *lv, int pinned)
813 {
814 ✗ lv->pinned = pinned;
815 ✗ if (pinned) {
816 ✗ SDL_StartTextInput();
817 ✗ lv->typing = 1;
818 } else {
819 ✗ SDL_StopTextInput();
820 ✗ lv->last_input = av_gettime_relative() - LIST_HOLD_TIME;
821 }
822 ✗ lv->render_pending = 1;
823 ✗ }
824
825 ✗ int list_view_pinned(const ListView *lv)
826 {
827 ✗ return lv->pinned;
828 }
829
830 ✗ int list_view_visible(const ListView *lv)
831 {
832 ✗ return lv->pinned || list_alpha(lv, av_gettime_relative()) > 0;
833 }
834
835 ✗ int list_view_selected(const ListView *lv)
836 {
837 ✗ return lv->nb_rows ? lv->rows[lv->selected] : -1;
838 }
839
840 ✗ void list_view_move(ListView *lv, int delta)
841 {
842 ✗ if (lv->nb_rows)
843 ✗ list_select(lv, av_clip(lv->selected + delta, 0, lv->nb_rows - 1));
844 ✗ }
845
846 ✗ int list_view_key(ListView *lv, SDL_Keycode key)
847 {
848 ✗ switch (key) {
849 ✗ case SDLK_ESCAPE: list_view_pin(lv, 0); break;
850 ✗ case SDLK_UP: list_view_move(lv, -1); break;
851 ✗ case SDLK_DOWN: list_view_move(lv, 1); break;
852 ✗ case SDLK_BACKSPACE: list_view_text(lv, NULL); break;
853 ✗ case SDLK_RETURN:
854 ✗ case SDLK_KP_ENTER: return list_view_selected(lv);
855 }
856 ✗ return -1;
857 }
858
859 ✗ void list_view_clear_search(ListView *lv)
860 {
861 ✗ lv->search[0] = 0;
862 ✗ list_update_rows(lv);
863 ✗ lv->render_pending = 1;
864 ✗ }
865
866 /* asks for a render when what a visible list points at changed; the render waits until the pending events are handled,
867 * so a sweep across the list renders once */
868 ✗ void list_view_hover(ListView *lv, int x, int y)
869 {
870 ListLayout l;
871 ListHit hit;
872
873 ✗ lv->mouse_x = x;
874 ✗ lv->mouse_y = y;
875 ✗ if (!list_view_visible(lv))
876 ✗ return;
877 ✗ l = list_layout(lv);
878 ✗ hit = list_hit(&l, x, y);
879 ✗ if (hit.row != lv->hover.row || hit.button != lv->hover.button || hit.detail != lv->hover.detail)
880 ✗ lv->render_pending = 1;
881 }
882
883 ✗ int list_view_click(ListView *lv, int x, int y, int *chosen)
884 {
885 ✗ ListLayout l = list_layout(lv);
886 ✗ ListHit hit = list_hit(&l, x, y);
887
888 ✗ *chosen = hit.row >= 0 && !hit.detail ? lv->rows[hit.row] : -1;
889 ✗ if (hit.button >= 0) {
890 ✗ int button_w = sort_buttons[hit.button].width * l.font / 2;
891
892 ✗ list_sort_by(lv, hit.button, (x - l.font - l.button_x[hit.button]) * 2 < button_w ? -1 : 1);
893 ✗ list_update_rows(lv);
894 ✗ list_select(lv, lv->selected);
895 }
896 ✗ return hit.inside;
897 }
898
899 ✗ void list_view_set_size(ListView *lv, int width, int height)
900 {
901 ✗ if (lv->width == width && lv->height == height)
902 ✗ return;
903 ✗ lv->width = width;
904 ✗ lv->height = height;
905 ✗ if (list_view_visible(lv))
906 ✗ lv->render_pending = 1;
907 else
908 ✗ list_discard(lv);
909 }
910
911 ✗ void list_view_set_playing(ListView *lv, int entry)
912 {
913 ✗ if (lv->playing == entry)
914 ✗ return;
915 ✗ lv->playing = entry;
916 ✗ lv->render_pending = 1;
917 }
918
919 ✗ int list_view_update(ListView *lv)
920 {
921 ✗ double alpha = list_alpha(lv, av_gettime_relative());
922 int changed;
923
924 ✗ if (lv->typing && !lv->pinned && alpha <= 0) {
925 ✗ lv->typing = 0;
926 ✗ lv->search[0] = 0;
927 ✗ list_update_rows(lv);
928 }
929 ✗ if (lv->render_pending && lv->width)
930 ✗ list_render(lv);
931 ✗ changed = lv->changed || alpha != lv->drawn_alpha;
932 ✗ lv->changed = 0;
933 ✗ return changed;
934 }
935
936 /* uploads what the texture lacks of the bitmap, at most once per refresh */
937 ✗ void list_view_draw(ListView *lv)
938 {
939 ✗ double alpha = list_alpha(lv, av_gettime_relative());
940 ✗ SDL_Rect *dirty = &lv->dirty;
941
942 ✗ lv->drawn_alpha = alpha;
943 ✗ if (alpha <= 0)
944 ✗ return;
945 ✗ if (dirty->w && !SDL_UpdateTexture(lv->texture, dirty, lv->bitmap + (dirty->y * lv->rect.w + dirty->x) * 4, lv->rect.w * 4))
946 ✗ *dirty = (SDL_Rect){ 0, 0, 0, 0 };
947 ✗ SDL_SetTextureAlphaMod(lv->texture, alpha * 255);
948 ✗ SDL_RenderCopy(lv->renderer, lv->texture, NULL, &lv->rect);
949 }
950