From c1c58d33b9338ef7a7e3e9bae352b5c30ba4c9e1 Mon Sep 17 00:00:00 2001 From: Krzosa Karol Date: Mon, 28 Jul 2025 14:12:39 +0200 Subject: [PATCH] transcript browser working --- src/core/core_arena.c | 8 + src/core/core_arena.h | 2 + src/os/os_win32.c | 3 + src/prototype/main.c | 10 +- src/prototype/transcript_browser.c | 504 +++++++++++++++++++++-------- 5 files changed, 389 insertions(+), 138 deletions(-) diff --git a/src/core/core_arena.c b/src/core/core_arena.c index 1c6789c..98f59b0 100644 --- a/src/core/core_arena.c +++ b/src/core/core_arena.c @@ -175,3 +175,11 @@ fn ma_temp_t ma_begin_scratch2(ma_arena_t *a, ma_arena_t *b) { ma_arena_t *conflicts[] = {a, b}; return ma_begin_scratch_ex(conflicts, 2); } + +fn void *ma_push__copy(ma_arena_t *arena, void *data, u64 size) { + void *result = ma_push_size_ex(arena, size); + if (result) { + memory_copy(result, data, size); + } + return result; +} diff --git a/src/core/core_arena.h b/src/core/core_arena.h index ad7907d..bacb207 100644 --- a/src/core/core_arena.h +++ b/src/core/core_arena.h @@ -32,6 +32,8 @@ fn ma_arena_t *ma_push_arena(ma_arena_t *allocator, usize size); fn void ma_push_arena_ex(ma_arena_t *allocator, ma_arena_t *result, usize size); #define ma_push_type(arena, Type) (Type *)ma_push_size((arena), sizeof(Type)) #define ma_push_array(arena, Type, count) (Type *)ma_push_size((arena), sizeof(Type) * (count)) +#define ma_push_type_copy(ma, data) ma_push__copy((ma), (data), sizeof(*(data))) +#define ma_push_array_copy(ma, data, count) ma_push__copy((ma), (data), (count) * sizeof(*(data))) // // pop diff --git a/src/os/os_win32.c b/src/os/os_win32.c index a1af442..e9cc392 100644 --- a/src/os/os_win32.c +++ b/src/os/os_win32.c @@ -159,10 +159,13 @@ fn void os_advance(os_iter_t *it) { it->is_directory = data->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; it->name = s8_from_s16(it->arena, s16_make(data->cFileName, str16_len(data->cFileName))); + char *is_dir = it->is_directory ? "/" : ""; char *separator = it->path.str[it->path.len - 1] == '/' ? "" : "/"; it->rel = s8_printf(it->arena, "%S%s%S%s", it->path, separator, it->name, is_dir); it->abs = os_abs(it->arena, it->rel); + s8_normalize_path_unsafe(it->rel); + s8_normalize_path_unsafe(it->abs); it->is_valid = true; if (it->is_directory) { diff --git a/src/prototype/main.c b/src/prototype/main.c index 53361de..f9e150a 100644 --- a/src/prototype/main.c +++ b/src/prototype/main.c @@ -20,9 +20,13 @@ fn_export b32 app_update(thread_ctx_t *thread_ctx, app_frame_t *frame) { mt_tweak_f32(font_size, 30, 4, 200); mt_tweak_f32(_font_size, 30, 30, 30); - ma_arena_t *perm = &tcx->perm; - rn_init(perm, font_size, frame->dpr); - ui_init(perm); + rn_init(&tcx->perm, font_size, frame->dpr); + ui_init(&tcx->perm); + + global_res = ma_push_type(&tcx->perm, res_t); + res_init(global_res); + res_add_folder(global_res, s8("D:/videos/jiang")); + // res_add_folder(global_res, s8("D:/tools")); return true; } else if (frame->first_event->kind == app_event_kind_reload) { ui_reload(); diff --git a/src/prototype/transcript_browser.c b/src/prototype/transcript_browser.c index 60f0913..479f93f 100644 --- a/src/prototype/transcript_browser.c +++ b/src/prototype/transcript_browser.c @@ -1,103 +1,8 @@ -fn void transcript_browser_update(app_frame_t *frame, mt_tweak_t *tweak_table, i32 tweak_count) { - ui_begin_frame(frame); - rn_begin_frame(frame); - - - s8_t cmds[] = { - s8("show data tab"), - s8("show log tab"), - s8("show menus tab"), - s8("show menus tab2"), - s8("show menus tab3"), - s8("show menus tab4"), - s8("show menus tab5"), - s8("show menus tab6"), - s8("show menus tab8"), - s8("show menus tab7"), - s8("show menus tab9"), - s8("show menus tab0"), - s8("show menus tab11"), - s8("show menus tab22"), - s8("show menus tab33"), - s8("show menus tab44"), - s8("show menus tab55"), - s8("show menus tab66"), - }; - - for (app_event_t *ev = frame->first_event; ev; ev = ev->next) { - ui_begin_build(UILOC, ev, window_rect_from_frame(frame)); - - locl b8 set_focus; - locl char buff[128]; - locl ui_text_input_t text_input; - ui_signal_t text_input_sig; - text_input.str = buff; - text_input.cap = lengthof(buff); - - ui_box_t *top_box = ui_box(.null_id = true, .rect = r2f32_cut_top(ui_top_rectp(), ui_em(1.0f)), .flags = {.draw_rect = true, .clip_rect = true}); - ui_set_text_align(ui_text_align_left) - ui_set_lop(ui_lop_cut_top) - ui_set_top(top_box) { - text_input_sig = ui_text_input(&text_input, .sim_even_if_no_focus = true, .keyboard_nav = false); - } - - ma_temp_t scratch = ma_begin_scratch(); - s8_t needle = s8_make(text_input.str, text_input.len); - fuzzy_pair_t *pairs = s8_fuzzy_rate_array(scratch.arena, needle, cmds, lengthof(cmds)); - if (text_input_sig.text_changed) { - set_focus = true; - } - - ui_box_t *lister = ui_box(.null_id = true, .rect = r2f32_cut_top(ui_top_rectp(), ui_max), .flags = {.draw_rect = true, .clip_rect = true}); - locl f32 verti_scroller_value; - ui_scroller_t scroller = ui_begin_scroller(UILOC, (ui_scroller_params_t){ - .parent = lister, - .verti = { - .enabled = true, - .value = &verti_scroller_value, - .item_pixels = ui_em(1), - .item_count = (i32)lengthof(cmds), - }, - }); - - ui_set_top(lister) - ui_set_string_pos_offset(ui_dm(1)) { - ui_cut_top_scroller_offset(scroller); - for (i32 i = scroller.verti.istart; i < scroller.verti.iend; i += 1) { - s8_t text = cmds[pairs[i].index]; - ui_box_t *box = ui_box(.string = text, .flags = { .draw_rect = true, .draw_text = true, .keyboard_nav = true }); - ui_signal_from_box(box); - - if (set_focus) { - ui->focus = box->id; - set_focus = false; - } - } - } - - if (text_input_sig.text_commit) { - fuzzy_pair_t *pair = &pairs[0]; - // ui_g_panel = (i32)pair->index + 1; - ui_text_clear(&text_input); - set_focus = true; - } - - ui_end_scroller(scroller); - ui_end_build(); - ma_end_scratch(scratch); - } - - rn_begin(white_color); - ui_draw(); - rn_end(); - ui_end_frame(); -} - /////////////////////////////// // work queue -#define WORK_FUNCTION(name) void name(void *data) -typedef WORK_FUNCTION(work_queue_callback_t); +#define FN_WORK(name) void name(void *data) +typedef FN_WORK(work_queue_callback_t); typedef struct { work_queue_callback_t *callback; @@ -106,32 +11,32 @@ typedef struct { typedef struct work_queue_t work_queue_t; struct work_queue_t { - int32_t thread_count; - work_queue_entry_t entries[256]; - int64_t volatile index_to_write; - int64_t volatile index_to_read; - int64_t volatile completion_index; - int64_t volatile completion_goal; + i32 thread_count; + work_queue_entry_t entries[1024]; + i64 volatile index_to_write; + i64 volatile index_to_read; + i64 volatile completion_index; + i64 volatile completion_goal; void *semaphore; }; typedef struct thread_startup_info_t thread_startup_info_t; struct thread_startup_info_t { - uint32_t thread_id; - int32_t thread_index; + u32 thread_id; + i32 thread_index; work_queue_t *queue; }; -fn int64_t atomic_increment(volatile int64_t *i) { +fn i64 atomic_increment(volatile i64 *i) { return InterlockedIncrement64(i); } -fn int64_t atomic_compare_and_swap(volatile int64_t *dst, int64_t exchange, int64_t comperand) { +fn i64 atomic_compare_and_swap(volatile i64 *dst, i64 exchange, i64 comperand) { return InterlockedCompareExchange64(dst, exchange, comperand); } fn void work_queue_push(work_queue_t *wq, void *data, work_queue_callback_t *callback) { - uint32_t new_index = (wq->index_to_write + 1) % lengthof(wq->entries); + u32 new_index = (wq->index_to_write + 1) % lengthof(wq->entries); assert(new_index != wq->index_to_read); work_queue_entry_t *entry = wq->entries + wq->index_to_write; @@ -146,10 +51,10 @@ fn void work_queue_push(work_queue_t *wq, void *data, work_queue_callback_t *cal fn b8 work_queue_try_doing_work(work_queue_t *wq) { b8 should_sleep = false; - int64_t original_index_to_read = wq->index_to_read; - int64_t new_index_to_read = (original_index_to_read + 1) % lengthof(wq->entries); + i64 original_index_to_read = wq->index_to_read; + i64 new_index_to_read = (original_index_to_read + 1) % lengthof(wq->entries); if (original_index_to_read != wq->index_to_write) { - int64_t index = atomic_compare_and_swap(&wq->index_to_read, new_index_to_read, original_index_to_read); + i64 index = atomic_compare_and_swap(&wq->index_to_read, new_index_to_read, original_index_to_read); if (index == original_index_to_read) { work_queue_entry_t *entry = wq->entries + index; entry->callback(entry->data); @@ -164,7 +69,8 @@ fn b8 work_queue_try_doing_work(work_queue_t *wq) { fn DWORD WINAPI work_queue_thread_entry(LPVOID param) { thread_startup_info_t *ti = (thread_startup_info_t *)param; - os_core_init(); + tcx = &global_thread_context; + os_win32_register_crash_handler(); tcx->thread_index = ti->thread_index; for (;;) { if (work_queue_try_doing_work(ti->queue)) { @@ -173,7 +79,7 @@ fn DWORD WINAPI work_queue_thread_entry(LPVOID param) { } } -fn void work_queue_init(work_queue_t *queue, uint32_t thread_count, thread_startup_info_t *info) { +fn void work_queue_init(work_queue_t *queue, thread_startup_info_t *info, u32 thread_count) { queue->thread_count = thread_count; queue->index_to_read = 0; queue->index_to_write = 0; @@ -182,7 +88,7 @@ fn void work_queue_init(work_queue_t *queue, uint32_t thread_count, thread_start queue->semaphore = CreateSemaphoreExA(0, 0, thread_count, 0, 0, SEMAPHORE_ALL_ACCESS); assert(queue->semaphore != INVALID_HANDLE_VALUE); - for (uint32_t i = 0; i < thread_count; i++) { + for (u32 i = 0; i < thread_count; i++) { thread_startup_info_t *ti = info + i; ti->thread_index = i; ti->queue = queue; @@ -206,6 +112,52 @@ fn void work_queue_wait(work_queue_t *wq) { } } +/////////////////////////////// +// process +typedef struct process_t process_t; +struct process_t { + b8 is_valid; + char platform[32]; +}; + +fn process_t process_run(s8_t args) { + ma_temp_t scratch = ma_begin_scratch(); + wchar_t *application_name = NULL; + + wchar_t *cmd = s16_from_s8(scratch.arena, args).str; + BOOL inherit_handles = FALSE; + DWORD creation_flags = 0; + void *enviroment = NULL; + wchar_t *working_dir = NULL; + STARTUPINFOW startup_info = {}; + startup_info.cb = sizeof(STARTUPINFOW); + process_t result = {}; + assert(sizeof(result.platform) >= sizeof(PROCESS_INFORMATION)); + PROCESS_INFORMATION *process_info = (PROCESS_INFORMATION *)result.platform; + BOOL success = CreateProcessW(application_name, cmd, NULL, NULL, inherit_handles, creation_flags, enviroment, working_dir, &startup_info, process_info); + result.is_valid = true; + if (!success) { + result.is_valid = false; + + LPVOID lpMsgBuf; + DWORD dw = GetLastError(); + FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL); + + char *buff = (char *)lpMsgBuf; + size_t buffLen = strlen((const char *)buff); + if (buffLen > 0 && buff[buffLen - 1] == '\n') { + buff[buffLen - 1] = 0; + } + + debugf("failed to create process | message: %s | cmd: %s", (char *)lpMsgBuf, args); + LocalFree(lpMsgBuf); + } + + ma_end_scratch(scratch); + return result; +} + /////////////////////////////// // mutex @@ -244,18 +196,13 @@ fn void mutex_unlock(mutex_t mutex) { typedef struct srt_entry_t srt_entry_t; struct srt_entry_t { - srt_entry_t *next; u16 hour, minute, second; s8_t string; + s8_t filepath; }; +typedef array(srt_entry_t) array_srt_entry_t; -typedef struct srt_result_t srt_result_t; -struct srt_result_t { - srt_entry_t *first; - srt_entry_t *last; -}; - -srt_result_t srt_parse(ma_arena_t *arena, s8_t filename) { +fn array_srt_entry_t srt_parse(ma_arena_t *arena, s8_t filename) { s8_t content = os_read(arena, filename); sb8_t lines = s8_split(arena, content, s8("\n"), s8_split_cleanup); @@ -269,7 +216,8 @@ srt_result_t srt_parse(ma_arena_t *arena, s8_t filename) { // time interval // text int section_number = 1; - srt_result_t result = {0}; + array_srt_entry_t result = {0}; + result.alo = malo(arena); for (sb8_node_t *it = lines.first; it;) { // parse section number long num = strtol(it->str, NULL, 10); @@ -282,17 +230,16 @@ srt_result_t srt_parse(ma_arena_t *arena, s8_t filename) { entry.hour = (u16)strtol(it->str, NULL, 10); entry.minute = (u16)strtol(it->str + 3, NULL, 10); entry.second = (u16)strtol(it->str + 6, NULL, 10); + entry.filepath = filename; it = it->next; // parse text s8_t next_section_number = s8_printf(arena, "%d", section_number); while (it && !s8_are_equal(next_section_number, it->string)) { - b8 duplicate = result.last && s8_are_equal(it->string, result.last->string); + b8 duplicate = result.len && s8_are_equal(it->string, array_last(&result).string); if (!duplicate) { - srt_entry_t *entry_copy = ma_push_type(arena, srt_entry_t); - entry_copy[0] = entry; - entry_copy->string = it->string; - SLLQ_APPEND(result.first, result.last, entry_copy); + entry.string = it->string; + array_add(&result, entry); } it = it->next; } @@ -301,14 +248,301 @@ srt_result_t srt_parse(ma_arena_t *arena, s8_t filename) { return result; } -fn_test void testing_out_things(void) { - mutex_t mutex = mutex_create(); - mutex_lock(mutex); - mutex_unlock(mutex); - mutex_destroy(mutex); +/////////////////////////////// +// multithreaded resource loading +typedef array(s8_t) array_s8_t; +typedef struct res_parse_work_t res_parse_work_t; +typedef struct res_t res_t; +struct res_parse_work_t { + s8_t srt; + res_t *res; +}; + +struct res_t { + ma_arena_t misc_arena; + array_s8_t error_messages; + + mutex_t mutex; + ma_arena_t text_arena; + ma_arena_t node_arena; + array_srt_entry_t mapping_entries; + + thread_startup_info_t thread_info[16]; + work_queue_t work_queue; + mutex_t log_mutex; + + i64 search_stop_counter; + mutex_t search_mutex; + array_s8_t search_matches; + ui_text_input_t search_text_input; + char search_prompt[256]; +}; + +#define res_report(res, ...) (mutex_lock((res)->log_mutex), array_add(&(res)->error_messages, s8_printf(&(res)->misc_arena, __VA_ARGS__)), mutex_unlock((res)->log_mutex)) + +fn void res_init(res_t *res) { + work_queue_init(&res->work_queue, res->thread_info, lengthof(res->thread_info)); + ma_init(&res->text_arena, ma_default_reserve_size); + ma_init(&res->node_arena, ma_default_reserve_size); + ma_init(&res->misc_arena, ma_default_reserve_size); + res->log_mutex = mutex_create(); + res->mutex = mutex_create(); + res->search_mutex = mutex_create(); + array_init(malo(&res->misc_arena), &res->search_matches, 10000000); + array_init(malo(&res->misc_arena), &res->error_messages, 10000); + res->text_arena.align = 0; +} + +fn s8_t srt_find_matching_video(s8_t filename) { + s8_t no_srt = s8_chop_last_period(filename); + if (s8_ends_with(no_srt, s8(".en"))) { + no_srt = s8_chop_last_period(no_srt); + } + char *ext[] = {"mp4", "webm", "mkv"}; + for (i32 i = 0; i < lengthof(ext); i += 1) { + s8_t video = s8_printf(tcx->temp, "%S.%s", no_srt, ext[i]); + if (os_exists(video)) { + return video; + } + } + return s8_null; +} + +FN_WORK(res_parse_thread) { + res_parse_work_t *pair = (res_parse_work_t *)data; ma_temp_t scratch = ma_begin_scratch(); - srt_result_t result = srt_parse(scratch.arena, s8("D:\\videos\\zizek\\‘Hegel and the Spirit of Distrust’ Sven-Olov Wallenstein & Slavoj Žižek in Conversation [a7gEN-Rxr4c].en.srt")); - unused(result); + array_srt_entry_t srt = srt_parse(scratch.arena, pair->srt); + mutex_lock(pair->res->mutex); + pair->res->mapping_entries.alo = malo(&pair->res->node_arena); + for (i64 i = 0; i < srt.len; i += 1) { + srt_entry_t it = srt.data[i]; + it.filepath = s8_copy(&pair->res->misc_arena, it.filepath); + it.string = s8_copy(&pair->res->text_arena, it.string); + it.string.str[it.string.len] = ' '; + array_add(&pair->res->mapping_entries, it); + } + mutex_unlock(pair->res->mutex); ma_end_scratch(scratch); + + if (srt.len) { + res_report(pair->res, "%S", pair->srt); + } else { + res_report(pair->res, "failed to load: %S", pair->srt); + } +} + +fn void res_add_folder(res_t *res, s8_t folder) { + ma_temp_t scratch = ma_begin_scratch(); + + if (!os_is_dir(folder)) { + res_report(res, "failed to find folder: %S", folder); + } + for (os_iter_t *iter = os_iter(scratch.arena, folder); iter->is_valid; os_advance(iter)) { + if (iter->is_directory) { + continue; + } + if (!s8_ends_with(iter->abs, s8(".srt"))) { + continue; + } + // @leak + res_parse_work_t *work = ma_push_type(&res->misc_arena, res_parse_work_t); + work->srt = iter->abs; + work->res = res; + work_queue_push(&res->work_queue, work, res_parse_thread); + } + + ma_end_scratch(scratch); +} + +fn srt_entry_t *res_find_entry(res_t *res, s8_t res_string) { + srt_entry_t *result = NULL; + mutex_lock(res->mutex); + for (srt_entry_t *it = res->mapping_entries.data; it != res->mapping_entries.data + res->mapping_entries.len; it += 1) { + u64 begin = (u64)(it->string.str); + u64 end = (u64)(it->string.str + it->string.len); + u64 needle = (u64)(res_string.str); + if (needle >= begin && needle < end) { + result = it; + break; + } + } + mutex_unlock(res->mutex); + return result; +} + +fn s8_t res_get_string(res_t *res) { + mutex_lock(res->mutex); + s8_t result = (s8_t){res->text_arena.data, res->text_arena.len}; + mutex_unlock(res->mutex); + return result; +} + +/////////////////////////////// +// search thread + +FN_WORK(res_search_thread) { + res_t *res = (res_t *)data; + + if (res->search_text_input.len == 0) { + return; + } + + i64 search_stop_counter = res->search_stop_counter; + mutex_lock(res->search_mutex); + res->search_matches.len = 0; + mutex_unlock(res->search_mutex); + + s8_t buffer = res_get_string(res); + s8_t find = res->search_text_input.string; + i64 index = 0; + while (s8_seek(buffer, find, s8_seek_ignore_case, &index)) { + if (search_stop_counter != res->search_stop_counter) { + break; + } + assert(res->search_matches.cap == 10000000); + s8_t found = s8_make(buffer.str + index, find.len); + array_add(&res->search_matches, found); + buffer = s8_skip(buffer, index + find.len); + } +} + +fn void res_search_start(res_t *res) { + res->search_stop_counter += 1; + work_queue_push(&res->work_queue, res, res_search_thread); +} + +fn array_s8_t res_search_get(ma_arena_t *arena, res_t *res) { + mutex_lock(res->search_mutex); + array_s8_t copy = res->search_matches; + copy.data = ma_push_array_copy(arena, copy.data, copy.len); + mutex_unlock(res->search_mutex); + return copy; +} + +res_t *global_res = NULL; +// @todo @core make temp arena normal pointer in ctx, dont init on win32 etc. +// @todo @core make it sure that not too much arenas are initialized for threads +// @todo @ui scrolling when focused button goes out of screen +fn_test void testing_out_things(void) { + +} + +/////////////////////////////// +// update + +fn void transcript_browser_update(app_frame_t *frame, mt_tweak_t *tweak_table, i32 tweak_count) { + ui_begin_frame(frame); + rn_begin_frame(frame); + + res_t *res = global_res; + for (app_event_t *ev = frame->first_event; ev; ev = ev->next) { + ui_begin_build(UILOC, ev, window_rect_from_frame(frame)); + + locl b8 set_focus; + ui_signal_t text_input_sig; + res->search_text_input.str = res->search_prompt; + res->search_text_input.cap = lengthof(res->search_prompt); + + ui_box_t *top_box = ui_box(.null_id = true, .rect = r2f32_cut_top(ui_top_rectp(), ui_em(1.0f)), .flags = {.draw_rect = true, .clip_rect = true}); + ui_set_text_align(ui_text_align_left) + ui_set_lop(ui_lop_cut_top) + ui_set_top(top_box) { + text_input_sig = ui_text_input(&res->search_text_input, .sim_even_if_no_focus = true, .keyboard_nav = false); + } + + ma_temp_t scratch = ma_begin_scratch(); + + + array_s8_t matches = res_search_get(scratch.arena, res); + if (matches.len == 0 || res->search_text_input.len == 0) { + // @concurrency not sure if this is ok + matches = res->error_messages; + } + + ui_box_t *lister = ui_box(.null_id = true, .rect = r2f32_cut_top(ui_top_rectp(), ui_max), .flags = {.draw_rect = true, .clip_rect = true}); + locl f32 verti_scroller_value; + ui_scroller_t scroller = ui_begin_scroller(UILOC, (ui_scroller_params_t){ + .parent = lister, + .verti = { + .enabled = true, + .value = &verti_scroller_value, + .item_pixels = ui_em(1), + .item_count = (i32)matches.len, + }, + }); + + ui_set_top(lister) + ui_set_string_pos_offset(ui_dm(1)) { + ui_cut_top_scroller_offset(scroller); + for (i32 i = scroller.verti.istart; i < scroller.verti.iend; i += 1) { + if (i >= matches.len) { + break; + } + ui_push_id((ui_id_t){i}); + s8_t it = matches.data[i]; + + uintptr_t begin_region = (uintptr_t)res->text_arena.data; + uintptr_t end_region = (uintptr_t)res->text_arena.data + res->text_arena.len; + + u64 chars_per_line = (i64)(frame->window_size.x) / (i64)ui_dm(1) - strlen(res->search_prompt) * 2; + + u64 begin = (uintptr_t)(it.str - chars_per_line / 2); + u64 end = (uintptr_t)(it.str + it.len + chars_per_line / 2); + + u64 a = CLAMP(begin, begin_region, end_region); + u64 b = CLAMP((uintptr_t)it.str, begin_region, end_region); + s8_t left = {(char *)a, (i64)(b - a)}; + + u64 c = CLAMP((uintptr_t)(it.str + it.len), begin_region, end_region); + u64 d = CLAMP(end, begin_region, end_region); + s8_t right = {(char *)c, (i64)(d - c)}; + + s8_t middle = it; + s8_t string = s8_printf(scratch.arena, "%S**%S**%S", left, middle, right); + + ui_box_t *box = ui_box(.string = string, .flags = {.draw_rect = true, .draw_text = true, .keyboard_nav = true}); + ui_signal_t sig = ui_signal_from_box(box); + if (sig.clicked && matches.data != res->error_messages.data) { + srt_entry_t *entry = res_find_entry(res, middle); + s8_t video = srt_find_matching_video(entry->filepath); + if (video.len == 0) { + res_report(res, "failed to find video for: %S", entry->filepath); + } else { + int seconds = entry->hour * 60 * 60 + entry->minute * 60 + entry->second; + for (i64 i = 0; i < video.len; i += 1) { + if (video.str[i] == '/') video.str[i] = '\\'; + } + s8_t cmd = s8_printf(scratch.arena, "\"C:/Program Files/VideoLAN/VLC/vlc.exe\" --start-time %d \"%S\"", seconds, video); + debugf("%S\n", cmd); + process_run(cmd); + } + } + + if (set_focus) { + ui->focus = box->id; + set_focus = false; + } + + ui_pop_id(); + } + } + + if (text_input_sig.text_changed) { + res_search_start(res); + set_focus = true; + } + + assert(res->error_messages.cap == 10000); + assert(res->search_matches.cap == 10000000); + + ui_end_scroller(scroller); + ui_end_build(); + ma_end_scratch(scratch); + } + + rn_begin(white_color); + ui_draw(); + rn_end(); + ui_end_frame(); } \ No newline at end of file