diff --git a/README.md b/README.md index d1366ce..f1262b5 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ nk_console* nk_console_button_onclick(nk_console* parent, const char* text, void nk_console* nk_console_get_top(nk_console* widget); int nk_console_get_widget_index(nk_console* widget); void nk_console_check_tooltip(nk_console* console); -void nk_console_check_up_down(nk_console* widget, struct nk_rect bounds); +void nk_console_check_up_down(nk_console* widget); nk_bool nk_console_is_active_widget(nk_console* widget); void nk_console_set_active_parent(nk_console* new_parent); void nk_console_set_active_widget(nk_console* widget); diff --git a/demo/common/nuklear_console_demo.c b/demo/common/nuklear_console_demo.c index 8cbca06..30fa483 100644 --- a/demo/common/nuklear_console_demo.c +++ b/demo/common/nuklear_console_demo.c @@ -49,7 +49,8 @@ static int tree_option3 = 0; char list_view_labels[NK_CONSOLE_DEMO_LIST_VIEW_COUNT][32]; const char* list_view_event_get_label(struct nk_console* list_view, nk_uint index) { - if (index < 0 || index >= NK_CONSOLE_DEMO_LIST_VIEW_COUNT) { + NK_UNUSED(list_view); + if (index >= NK_CONSOLE_DEMO_LIST_VIEW_COUNT) { return NULL; } return list_view_labels[index]; diff --git a/demo/pntr/.cmake/Findpntr_app_starter.cmake b/demo/pntr/.cmake/Findpntr_app_starter.cmake index a72183a..29ac824 100644 --- a/demo/pntr/.cmake/Findpntr_app_starter.cmake +++ b/demo/pntr/.cmake/Findpntr_app_starter.cmake @@ -27,6 +27,10 @@ if (RAYLIB) ${SOURCES} ) + # Debug + target_compile_options(${project_name_raylib} PUBLIC -g) + set_target_properties(${project_name_raylib} PROPERTIES COMPILE_FLAGS "-g" LINK_FLAGS "-g") + target_link_libraries(${project_name_raylib} PUBLIC raylib_static ${LIBRARIES} diff --git a/demo/raylib/CMakeLists.txt b/demo/raylib/CMakeLists.txt index 6704104..b83c23a 100644 --- a/demo/raylib/CMakeLists.txt +++ b/demo/raylib/CMakeLists.txt @@ -70,6 +70,7 @@ endif() # Setup the example add_executable(${PROJECT_NAME} main.c) +target_compile_options(${PROJECT_NAME} PRIVATE -g) # Link dependencies target_link_libraries(${PROJECT_NAME} PUBLIC diff --git a/demo/sdl_renderer/Makefile b/demo/sdl_renderer/Makefile index a704f58..0db66df 100644 --- a/demo/sdl_renderer/Makefile +++ b/demo/sdl_renderer/Makefile @@ -4,7 +4,7 @@ BIN = nuklear_console_demo_sdl # Flags # Use C++ -CFLAGS += -std=c99 -pedantic -O0 +CFLAGS += -std=c99 -pedantic -O0 -g CXXFLAGS += -pedantic -O0 -fpermissive -std=c++17 CFLAGS += `sdl2-config --cflags` diff --git a/nuklear_console.h b/nuklear_console.h index ec727c7..982321e 100644 --- a/nuklear_console.h +++ b/nuklear_console.h @@ -109,8 +109,8 @@ typedef struct nk_console { typedef struct nk_console_top_data { nk_console* active_parent; /** The parent that is currently being displayed. */ nk_bool input_processed; /** Whether or not user input has been processed. */ - nk_bool scroll_requested; /** True if we've switched active widget and need to check scrolling */ nk_bool scrollbar_required; /** True when the scrollbar is needed to be rendered. @see nk_console_render_window() */ + nk_console* scroll_to_widget; /** When set by nk_console_navigate_back, the render loop scrolls the window to this widget's bounds. */ /** * Message queue that is to be shown. @@ -175,7 +175,7 @@ NK_API void nk_console_render_window(nk_console* console, const char* title, str NK_API nk_console* nk_console_get_top(nk_console* widget); NK_API int nk_console_get_widget_index(nk_console* widget); NK_API void nk_console_check_tooltip(nk_console* console); -NK_API void nk_console_check_up_down(nk_console* widget, struct nk_rect bounds); +NK_API void nk_console_check_up_down(nk_console* widget); NK_API nk_bool nk_console_is_active_widget(nk_console* widget); NK_API nk_console* nk_console_active_parent(nk_console* console); NK_API void nk_console_set_active_parent(nk_console* new_parent); @@ -561,26 +561,10 @@ NK_API int nk_console_get_widget_index(nk_console* widget) { /** * Allow the user to move up and down between widgets. */ -NK_API void nk_console_check_up_down(nk_console* widget, struct nk_rect bounds) { +NK_API void nk_console_check_up_down(nk_console* widget) { nk_console* top = nk_console_get_top(widget); nk_console_top_data* data = (nk_console_top_data*)top->data; - // Scroll to the active widget if needed. - if (data->scroll_requested) { - struct nk_rect content_region = nk_window_get_content_region(widget->ctx); - - nk_uint offsetx, offsety; - nk_window_get_scroll(widget->ctx, &offsetx, &offsety); - if (bounds.y + bounds.h > content_region.y + content_region.h + (float)offsety) { - nk_window_set_scroll(widget->ctx, offsetx, (nk_uint)(bounds.y + bounds.h - content_region.y - content_region.h)); - } - else if (bounds.y < content_region.y + (float)offsety) { - nk_window_set_scroll(widget->ctx, offsetx, (nk_uint)(bounds.y - content_region.y)); - } - - data->scroll_requested = nk_false; - } - // Handle hold-to-accelerate timers for up/down navigation. nk_bool up_held = nk_console_button_down(top, NK_GAMEPAD_BUTTON_UP); nk_bool down_held = nk_console_button_down(top, NK_GAMEPAD_BUTTON_DOWN); @@ -606,33 +590,43 @@ NK_API void nk_console_check_up_down(nk_console* widget, struct nk_rect bounds) // Page Up if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_LB)) { int widgetIndex = nk_console_get_widget_index(widget); + nk_console* target = NULL; int count = 0; while (--widgetIndex >= 0) { - nk_console* target = widget->parent->children[widgetIndex]; - if (target != NULL && nk_console_selectable(target)) { - nk_console_set_active_widget(target); - data->scroll_requested = nk_true; + nk_console* t = widget->parent->children[widgetIndex]; + if (t != NULL && nk_console_selectable(t)) { + target = t; if (++count > 4) { break; } } } + if (target != NULL) { + nk_console_set_active_widget(target); + data->scroll_to_widget = target; + } data->input_processed = nk_true; } // Page Down else if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_RB)) { int widgetIndex = nk_console_get_widget_index(widget); + nk_console* target = NULL; int count = 0; - while (++widgetIndex < (int)cvector_size(widget->parent->children)) { - nk_console* target = widget->parent->children[widgetIndex]; - if (nk_console_selectable(target)) { - nk_console_set_active_widget(target); - data->scroll_requested = nk_true; - if (++count > 4) { - break; + if (widget->parent != NULL) { + while (++widgetIndex < (int)cvector_size(widget->parent->children)) { + nk_console* t = widget->parent->children[widgetIndex]; + if (t != NULL && nk_console_selectable(t)) { + target = t; + if (++count > 4) { + break; + } } } } + if (target != NULL) { + nk_console_set_active_widget(target); + data->scroll_to_widget = target; + } data->input_processed = nk_true; } // Up @@ -641,9 +635,9 @@ NK_API void nk_console_check_up_down(nk_console* widget, struct nk_rect bounds) int widgetIndex = nk_console_get_widget_index(widget); while (--widgetIndex >= 0) { nk_console* target = widget->parent->children[widgetIndex]; - if (nk_console_selectable(target)) { + if (target != NULL && nk_console_selectable(target)) { nk_console_set_active_widget(target); - data->scroll_requested = nk_true; + data->scroll_to_widget = target; break; } } @@ -653,12 +647,14 @@ NK_API void nk_console_check_up_down(nk_console* widget, struct nk_rect bounds) else if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_DOWN) || (down_held && up_down_repeat_fire)) { int widgetIndex = nk_console_get_widget_index(widget); - while (++widgetIndex < (int)cvector_size(widget->parent->children)) { - nk_console* target = widget->parent->children[widgetIndex]; - if (nk_console_selectable(target)) { - nk_console_set_active_widget(target); - data->scroll_requested = nk_true; - break; + if (widget->parent != NULL) { + while (++widgetIndex < (int)cvector_size(widget->parent->children)) { + nk_console* target = widget->parent->children[widgetIndex]; + if (target != NULL && nk_console_selectable(target)) { + nk_console_set_active_widget(target); + data->scroll_to_widget = target; + break; + } } } data->input_processed = nk_true; @@ -666,12 +662,12 @@ NK_API void nk_console_check_up_down(nk_console* widget, struct nk_rect bounds) // Back else if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_B)) { if (nk_console_active_parent(top) == NULL) { + data->input_processed = nk_true; return; } if (widget->parent != NULL) { nk_console_navigate_back(widget->parent); - data->scroll_requested = nk_true; } data->input_processed = nk_true; @@ -913,6 +909,24 @@ NK_API void nk_console_render(nk_console* console) { // Render the widget and get its bounds. struct nk_rect widget_bounds = console->render != NULL ? console->render(console) : nk_rect(0, 0, 0, 0); + // When nk_console_navigate_back targeted this widget, scroll the window to show it. + if (widget_bounds.w > 0 && widget_bounds.h > 0) { + nk_console* top = nk_console_get_top(console); + nk_console_top_data* top_data = (nk_console_top_data*)top->data; + if (top_data->scroll_to_widget == console) { + struct nk_rect content_region = nk_window_get_content_region(console->ctx); + nk_uint offsetx, offsety; + nk_window_get_scroll(console->ctx, &offsetx, &offsety); + if (widget_bounds.y + widget_bounds.h > content_region.y + content_region.h + (float)offsety) { + nk_window_set_scroll(console->ctx, offsetx, (nk_uint)(widget_bounds.y + widget_bounds.h - content_region.y - content_region.h)); + } + else if (widget_bounds.y < content_region.y + (float)offsety) { + nk_window_set_scroll(console->ctx, offsetx, (nk_uint)(widget_bounds.y - content_region.y)); + } + top_data->scroll_to_widget = NULL; + } + } + // Allow the mouse to switch focus to the widget. if (widget_bounds.w > 0 && widget_bounds.h > 0 && nk_input_is_mouse_moved(&console->ctx->input)) { // Make sure we consider the active scroll position of the window. @@ -1183,6 +1197,12 @@ NK_API void nk_console_navigate_back(nk_console* leaving_parent) { } nk_console* destination = (leaving_parent->parent != NULL) ? leaving_parent->parent : top; nk_console_set_active_parent(destination); + nk_console_set_active_widget(leaving_parent); + nk_console_top_data* data = (nk_console_top_data*)top->data; + if (data != NULL) { + data->scroll_to_widget = leaving_parent; + data->input_processed = nk_true; + } nk_console_trigger_event(leaving_parent, NK_CONSOLE_EVENT_BACK); } diff --git a/nuklear_console_button.h b/nuklear_console_button.h index 9d15dde..7f3f401 100644 --- a/nuklear_console_button.h +++ b/nuklear_console_button.h @@ -171,7 +171,7 @@ NK_API struct nk_rect nk_console_button_render(nk_console* console) { // Allow switching up/down in widgets if (nk_console_is_active_widget(console)) { - nk_console_check_up_down(console, widget_bounds); + nk_console_check_up_down(console); nk_console_check_tooltip(console); } diff --git a/nuklear_console_checkbox.h b/nuklear_console_checkbox.h index 850b7a4..77bff47 100644 --- a/nuklear_console_checkbox.h +++ b/nuklear_console_checkbox.h @@ -106,7 +106,7 @@ NK_API struct nk_rect nk_console_checkbox_render(nk_console* console) { // Allow switching up/down in widgets if (nk_console_is_active_widget(console)) { - nk_console_check_up_down(console, widget_bounds); + nk_console_check_up_down(console); nk_console_check_tooltip(console); } diff --git a/nuklear_console_file.h b/nuklear_console_file.h index faa1a30..29cc126 100644 --- a/nuklear_console_file.h +++ b/nuklear_console_file.h @@ -9,6 +9,14 @@ #endif #endif // NK_CONSOLE_FILE_PATH_MAX +/** + * A single file or directory entry stored for the file widget's list view. + */ +typedef struct nk_console_file_entry { + char* label; /** The basename of the file or directory. */ + nk_bool is_directory; /** True if this entry is a directory. */ +} nk_console_file_entry; + /** * Custom data for the file widget. */ @@ -19,6 +27,8 @@ typedef struct nk_console_file_data { char directory[NK_CONSOLE_FILE_PATH_MAX]; /** When selecting a file, this is the current directory. */ void* file_user_data; /** Custom user data for the file system. */ nk_bool select_directory; /** Flag indicating if we are selecting a directory. */ + char dir_label_buf[NK_CONSOLE_FILE_PATH_MAX + 2]; /** Scratch buffer for appending "/" to directory labels in the list view. */ + nk_console_file_entry* entries; /** cvector of file/directory entries for the list view. */ } nk_console_file_data; #if defined(__cplusplus) @@ -41,8 +51,8 @@ NK_API nk_console* nk_console_file(nk_console* parent, const char* label, char* * Creates a directory widget that allows the user to select a directory. * * @param parent The parent widget. - * @param label The label for the file widget. For example: "Select a file". - * @param dir_buffer The buffer to store the file path. + * @param label The label for the file widget. For example: "Select a directory". + * @param dir_buffer The buffer to store the directory path. * @param dir_buffer_size The size of the buffer. * * @return The new file widget. @@ -74,7 +84,7 @@ NK_API void nk_console_file_set_file_user_data(nk_console* file, void* user_data NK_API void* nk_console_file_get_file_user_data(nk_console* file); /** - * Add a individual file or directory to the given file widget as a child. + * Add an individual file or directory entry to the given file widget's list view. * * This should be called from the file system callbacks. See `nuklear_console_file_system.h` for examples. * @@ -82,19 +92,17 @@ NK_API void* nk_console_file_get_file_user_data(nk_console* file); * @param path The path to the file or directory. * @param is_directory True if the path is a directory. False otherwise. * - * @return The new button if it was successfully added, NULL otherwise. + * @return Non-NULL if the entry was successfully added, NULL otherwise. * - * @see nk_console_file_destroy_tinydir() + * @see nk_console_file_add_files_tinydir() * @see nk_console_file_add_files_raylib() */ NK_API nk_console* nk_console_file_add_entry(nk_console* parent, const char* path, nk_bool is_directory); /** - * Refreshes the file widget to display the contents of the current directory. - * - * @param widget The file widget to refresh. + * Refreshes the file widget with the contents with its given directory. * - * @see nk_console_file_data::directory + * @internal */ NK_API void nk_console_file_refresh(nk_console* widget, void* user_data); @@ -116,13 +124,14 @@ extern "C" { /** * Gets the base name of a file path. + * + * @internal */ static const char* nk_console_file_basename(const char* path) { if (path == NULL) { return NULL; } - // TODO: Ensure UTF-8 compatibility. int len = nk_strlen(path); for (int i = len - 1; i > 0; i--) { if (path[i] == '\\' || path[i] == '/') { @@ -177,6 +186,8 @@ NK_API struct nk_rect nk_console_file_render(nk_console* console) { /** * Gets the file widget from a child button. + * + * @internal */ static nk_console* nk_console_file_button_get_file_widget(nk_console* button) { if (button == NULL) { @@ -196,35 +207,91 @@ static nk_console* nk_console_file_button_get_file_widget(nk_console* button) { } /** - * Free the individual file entry buttons. This clears the label. + * Free all file entries stored in the file data. + * + * @internal */ -NK_API void nk_console_file_free_entry(nk_console* button, void* user_data) { - NK_UNUSED(user_data); - if (button == NULL) { +static void nk_console_file_entries_clear(nk_console_file_data* data) { + if (data == NULL) { return; } - - if (button->label != NULL) { - nk_console_mfree(nk_handle_id(0), (void*)button->label); - button->label = NULL; + for (size_t i = 0; i < cvector_size(data->entries); i++) { + if (data->entries[i].label != NULL) { + nk_console_mfree(nk_handle_id(0), data->entries[i].label); + data->entries[i].label = NULL; + } } + cvector_clear(data->entries); } -NK_API void nk_console_file_entry_onclick(nk_console* button, void* user_data) { +/** + * Event handler: Destroy the file widget. + * + * @internal + */ +static void nk_console_file_event_destroy(nk_console* file, void* user_data) { NK_UNUSED(user_data); - if (button == NULL || button->label == NULL) { + if (file == NULL || file->data == NULL) { return; } + nk_console_file_data* data = (nk_console_file_data*)file->data; - nk_console* file = nk_console_file_button_get_file_widget(button); + // Clear all the file entries. + nk_console_file_entries_clear(data); + cvector_free(data->entries); + data->entries = NULL; +} + +#ifdef NK_CONSOLE_FILE_ADD_FILES +/** + * get_label callback for the file list view. Appends "/" to directory entries. + * + * @internal + */ +static const char* nk_console_file_list_view_get_label(struct nk_console* list_view, nk_uint index) { + nk_console* file = nk_console_file_button_get_file_widget(list_view); if (file == NULL || file->data == NULL) { - return; + return NULL; } - nk_console_file_data* data = (nk_console_file_data*)file->data; + if (cvector_empty(data->entries)) { + return "(Empty directory)"; + } + if (index >= cvector_size(data->entries)) { + return NULL; + } + nk_console_file_entry* entry = &data->entries[index]; + if (entry->is_directory) { + nk_size len = (nk_size)nk_strlen(entry->label); + if (len + 2 > sizeof(data->dir_label_buf)) { + return entry->label; + } + NK_MEMCPY(data->dir_label_buf, entry->label, len); + data->dir_label_buf[len] = '/'; + data->dir_label_buf[len + 1] = '\0'; + return data->dir_label_buf; + } + return entry->label; +} +#endif + +/** + * Appends a path component to data->directory, inserting the platform separator. + * Returns nk_false and shows an error if the result would overflow the buffer. + * + * @internal + */ +static nk_bool nk_console_file_append_to_directory(nk_console_file_data* data, nk_console* file, const char* label) { int len = nk_strlen(data->directory); + int label_len = nk_strlen(label); + + int dir_len_after_slash = (len == 1 && data->directory[0] == '.') ? 0 : (len > 0 ? len + 1 : 0); + if (dir_len_after_slash + label_len + 1 > NK_CONSOLE_FILE_PATH_MAX) { + NK_ASSERT(0); // Path too long + nk_console_show_message(file, "Error: File path is too long."); + return nk_false; + } - // Append a slash if the directory is not empty. if (len == 1 && data->directory[0] == '.') { len = 0; data->directory[0] = '\0'; @@ -240,48 +307,127 @@ NK_API void nk_console_file_entry_onclick(nk_console* button, void* user_data) { len++; } - // Concatenate the button label to the directory. // TODO: file: Resolve the path properly, so the paths don't recurse. For example: folder/../folder - // TODO: file: Add UTF-8 support. - NK_MEMCPY(data->directory + len, (void*)button->label, (nk_size)(nk_strlen(button->label) + 1)); - - enum nk_symbol_type symbol = nk_console_button_get_symbol(button); - switch (symbol) { // Directory - case NK_SYMBOL_TRIANGLE_LEFT: // Back - case NK_SYMBOL_TRIANGLE_RIGHT: // Folder - nk_console_set_active_parent(file); - nk_console_add_event(file, NK_CONSOLE_EVENT_POST_RENDER_ONCE, &nk_console_file_refresh); - break; - default: // File - { - // Copy the string to the file buffer. - // TODO: Ensure UTF-8 compatibility. - int desired_length = nk_strlen(data->directory); - if (desired_length >= data->file_path_buffer_size) { - NK_ASSERT(0); // File path is too long - nk_console_show_message(file, "Error: File path is too long."); - } - else { - NK_MEMCPY(data->file_path_buffer, data->directory, (nk_size)desired_length); - data->file_path_buffer[desired_length] = '\0'; - - // Trigger the onchange event and exit. - nk_console_trigger_event(file, NK_CONSOLE_EVENT_CHANGED); - } - - // Now that we selected a file, we can exit. - nk_console_set_active_parent(file->parent); - } break; + NK_MEMCPY(data->directory + len, label, (nk_size)(label_len + 1)); + return nk_true; +} + +#ifdef NK_CONSOLE_FILE_ADD_FILES +/** + * Click handler for the file list view. Navigates into directories or selects files. + * + * @internal + */ +static void nk_console_file_list_view_onclick(nk_console* list_view, void* user_data) { + NK_UNUSED(user_data); + nk_console* file = nk_console_file_button_get_file_widget(list_view); + if (file == NULL || file->data == NULL) { + return; + } + nk_console_file_data* data = (nk_console_file_data*)file->data; + + nk_console_list_view_data* lv_data = (nk_console_list_view_data*)list_view->data; + nk_uint selected = lv_data->selected; + if (selected >= cvector_size(data->entries)) { + return; + } + + nk_console_file_entry* entry = &data->entries[selected]; + if (!nk_console_file_append_to_directory(data, file, entry->label)) { + return; + } + + if (entry->is_directory) { + // Navigate into the directory. + nk_console_set_active_parent(file); + nk_console_add_event(file, NK_CONSOLE_EVENT_POST_RENDER_ONCE, &nk_console_file_refresh); + } + else { + // Copy the path to the file buffer. + int desired_length = nk_strlen(data->directory); + if (desired_length >= data->file_path_buffer_size) { + NK_ASSERT(0); // File path is too long + nk_console_show_message(file, "Error: File path is too long."); + } + else { + NK_MEMCPY(data->file_path_buffer, data->directory, (nk_size)desired_length); + data->file_path_buffer[desired_length] = '\0'; + nk_console_trigger_event(file, NK_CONSOLE_EVENT_CHANGED); + } + + // Exit the file browser. + nk_console_navigate_back(file); } } +#endif + +/** + * Click handler for the "select this directory" button in directory-selection mode. + * + * @internal + */ +static void nk_console_file_select_dir_onclick(nk_console* button, void* user_data) { + NK_UNUSED(user_data); + nk_console* file = nk_console_file_button_get_file_widget(button); + if (file == NULL || file->data == NULL) { + return; + } + nk_console_file_data* data = (nk_console_file_data*)file->data; + + int desired_length = nk_strlen(data->directory); + if (desired_length >= data->file_path_buffer_size) { + NK_ASSERT(0); // Directory path is too long + nk_console_show_message(file, "Error: Directory path is too long."); + } + else { + NK_MEMCPY(data->file_path_buffer, data->directory, (nk_size)desired_length); + data->file_path_buffer[desired_length] = '\0'; + nk_console_trigger_event(file, NK_CONSOLE_EVENT_CHANGED); + } + + // Exit the file browser. + nk_console_navigate_back(file); +} + +/** + * Event hanlder for when the user clicks on an individual file entry. + * + * @internal + */ +static void nk_console_file_entry_onclick(nk_console* button, void* user_data) { + NK_UNUSED(user_data); + if (button == NULL || button->label == NULL) { + return; + } + + nk_console* file = nk_console_file_button_get_file_widget(button); + if (file == NULL || file->data == NULL) { + return; + } + + nk_console_file_data* data = (nk_console_file_data*)file->data; + if (!nk_console_file_append_to_directory(data, file, button->label)) { + return; + } + + // Navigate into the directory (or back to parent via ".."). + nk_console_set_active_parent(file); + nk_console_add_event(file, NK_CONSOLE_EVENT_POST_RENDER_ONCE, &nk_console_file_refresh); +} NK_API nk_console* nk_console_file_add_entry(nk_console* parent, const char* path, nk_bool is_directory) { - if (parent == NULL || path == NULL || path[0] == '\0' || parent->data == NULL) { + if (parent == NULL || path == NULL || path[0] == '\0') { + return NULL; + } + + nk_console* file = nk_console_file_button_get_file_widget(parent); + if (file == NULL || file->data == NULL) { return NULL; } + nk_console_file_data* data = (nk_console_file_data*)file->data; + // Are we only selecting directories? - nk_console_file_data* data = (nk_console_file_data*)nk_console_file_button_get_file_widget(parent)->data; if (is_directory == nk_false && data->select_directory == nk_true) { return NULL; } @@ -297,35 +443,29 @@ NK_API nk_console* nk_console_file_add_entry(nk_console* parent, const char* pat return NULL; } - // Add the button. - nk_console* button = nk_console_button(parent, NULL); - - // Copy the path for the label, and register an event to destroy it. - // TODO: file: Ensure UTF-8 compatibility. - button->label = (const char*)NK_CONSOLE_MALLOC(nk_handle_id(0), NULL, (nk_size)(sizeof(char)) * (nk_size)(len + 1)); - nk_console_add_event(button, NK_CONSOLE_EVENT_DESTROYED, &nk_console_file_free_entry); - - char* label = (char*)button->label; - - // Use the base name as the label. + // Copy the basename as the entry label. const char* basename = nk_console_file_basename(path); nk_size basename_len = (nk_size)nk_strlen(basename); + char* label = (char*)NK_CONSOLE_MALLOC(nk_handle_id(0), NULL, basename_len + 1); + if (label == NULL) { + return NULL; + } NK_MEMCPY(label, basename, basename_len); label[basename_len] = '\0'; - // Symbol - if (is_directory == nk_true) { - nk_console_button_set_symbol(button, NK_SYMBOL_TRIANGLE_RIGHT); - } + nk_console_file_entry entry; + entry.label = label; + entry.is_directory = is_directory; + cvector_push_back(data->entries, entry); - // Event - nk_console_add_event(button, NK_CONSOLE_EVENT_CLICKED, &nk_console_file_entry_onclick); - return button; + // Return the file widget as a non-NULL success indicator. + return file; } - /** * Gets the length of the directory string of the given file path. + * + * @internal */ static int nk_console_file_get_directory_len(const char* file_path) { if (file_path == NULL) { @@ -341,7 +481,7 @@ static int nk_console_file_get_directory_len(const char* file_path) { } /** - * Fills the files array with the files in the current directory. + * Fills the list view with the files in the current directory. */ NK_API void nk_console_file_refresh(nk_console* widget, void* user_data) { NK_UNUSED(user_data); @@ -352,36 +492,54 @@ NK_API void nk_console_file_refresh(nk_console* widget, void* user_data) { nk_console_file_data* data = (nk_console_file_data*)widget->data; - // Clear out all the current entries. - nk_console_free_children(widget); - - // Add the back/cancel button - nk_console* cancelButton = nk_console_button_onclick(widget, "Cancel", &nk_console_button_back); - nk_console_button_set_symbol(cancelButton, NK_SYMBOL_X); + // Clear existing entries. + nk_console_file_entries_clear(data); + + // Build the static children (cancel, directory label, parent dir button) on the first call only. + // children[1]'s label points directly to data->directory, so it auto-updates on subsequent calls. + if (widget->children == NULL || cvector_empty(widget->children)) { + // Add the back/cancel button + nk_console* cancelButton = nk_console_button_onclick(widget, "Cancel", &nk_console_button_back); + nk_console_button_set_symbol(cancelButton, NK_SYMBOL_X); + + // Show the active directory. + if (!data->select_directory) { + // Active directory label + nk_console* activeLabel = nk_console_label(widget, data->directory); + activeLabel->alignment = NK_TEXT_CENTERED; + } + else { + // Add a button to select the current directory. + nk_console* button = nk_console_button(widget, data->directory); + nk_console_button_set_symbol(button, NK_SYMBOL_CIRCLE_SOLID); + nk_console_set_tooltip(button, "Use this directory"); + nk_console_add_event(button, NK_CONSOLE_EVENT_CLICKED, &nk_console_file_select_dir_onclick); + } - // Show the Active directory. - if (!data->select_directory) { - // Active directory label - nk_console* activeLabel = nk_console_label(widget, data->directory); - activeLabel->alignment = NK_TEXT_CENTERED; - } - else { - // Add the select directory button. - nk_console* button = nk_console_file_add_entry(widget, data->directory, nk_true); - nk_console_button_set_symbol(button, NK_SYMBOL_CIRCLE_SOLID); - nk_console_set_tooltip(button, "Use this directory"); + // Add the parent directory button + nk_console* parent_directory_button = nk_console_button_onclick(widget, "..", &nk_console_file_entry_onclick); + nk_console_button_set_symbol(parent_directory_button, NK_SYMBOL_TRIANGLE_LEFT); + nk_console_set_tooltip(parent_directory_button, "Navigate to the parent directory"); } - // Add the parent directory button - nk_console* parent_directory_button = nk_console_button_onclick(widget, "..", &nk_console_file_entry_onclick); - nk_console_button_set_symbol(parent_directory_button, NK_SYMBOL_TRIANGLE_LEFT); - nk_console_set_tooltip(parent_directory_button, "Navigate to the parent directory"); - nk_console_set_active_widget(parent_directory_button); + // Focus the parent directory button (always children[2]). + NK_ASSERT(cvector_size(widget->children) > 2 && widget->children[2] != NULL); + nk_console_set_active_widget(widget->children[2]); #ifdef NK_CONSOLE_FILE_ADD_FILES - // Iterate through the files in the directory, and add them as entries. - if (NK_CONSOLE_FILE_ADD_FILES(widget, data->directory) == nk_false) { - nk_console_label(widget, "No files found.")->alignment = NK_TEXT_CENTERED; + // Populate the entries array via the file system callback. + NK_CONSOLE_FILE_ADD_FILES(widget, data->directory); + + // Show at least 1 item so the get_label callback can display "(Empty directory)". + nk_uint display_count = cvector_empty(data->entries) ? 1 : (nk_uint)cvector_size(data->entries); + if (cvector_size(widget->children) >= 4 && widget->children[3]->type == NK_CONSOLE_LIST_VIEW) { + // Update the existing list view in place. + nk_console_list_view_set_item_count(widget->children[3], display_count); + } + else { + // Create the list view. + nk_console* list_view = nk_console_list_view(widget, "file_entries", 10, display_count, &nk_console_file_list_view_get_label); + nk_console_add_event(list_view, NK_CONSOLE_EVENT_CLICKED, &nk_console_file_list_view_onclick); } #else // NK_CONSOLE_FILE_ADD_FILES is undefined, so back out. @@ -396,9 +554,37 @@ NK_API void nk_console_file_refresh(nk_console* widget, void* user_data) { } /** - * Button callback for the main file button. + * Event handler to clear out unneeded data for the file widget. + * + * @see nk_console_file_event_back + * @internal + */ +static void nk_console_file_event_back_post_render(nk_console* file, void* user_data) { + // Remove all the children, since we don't need them. + nk_console_free_children(file); + + // Clear out all the file entries too. + nk_console_file_event_destroy(file, user_data); +} + +/** + * Event handler to clear out all unneeded data when not using the widget. + * @internal */ -static void nk_console_file_main_click(nk_console* button, void* user_data) { +static void nk_console_file_event_back(nk_console* file, void* user_data) { + NK_UNUSED(user_data); + // Clear it out at post-render to avoid segfaults. + nk_console_add_event(file, NK_CONSOLE_EVENT_POST_RENDER_ONCE, &nk_console_file_event_back_post_render); +} + +/** + * Event handler: Called when the make file button is clicked. + * + * Will build out the sub-elements to select a file. + * + * @internal + */ +static void nk_console_file_event_clicked(nk_console* button, void* user_data) { NK_UNUSED(user_data); if (button == NULL || button->data == NULL) { return; @@ -463,7 +649,10 @@ NK_API nk_console* nk_console_file(nk_console* parent, const char* label, char* widget->selectable = nk_true; widget->data = data; - nk_console_add_event(widget, NK_CONSOLE_EVENT_CLICKED, &nk_console_file_main_click); + nk_console_add_event(widget, NK_CONSOLE_EVENT_CLICKED, &nk_console_file_event_clicked); + nk_console_add_event(widget, NK_CONSOLE_EVENT_BACK, &nk_console_file_event_back); + nk_console_add_event(widget, NK_CONSOLE_EVENT_DESTROYED, &nk_console_file_event_destroy); + return widget; } diff --git a/nuklear_console_file_system.h b/nuklear_console_file_system.h index 55a3e86..aa02782 100644 --- a/nuklear_console_file_system.h +++ b/nuklear_console_file_system.h @@ -110,19 +110,15 @@ static nk_bool nk_console_file_add_files_raylib(nk_console* console, const char* nk_bool result = nk_false; - // Directories - for (int i = 0; i < filePathList.count; i++) { - if (DirectoryExists(filePathList.paths[i])) { - if (nk_console_file_add_entry(console, filePathList.paths[i], nk_true) != NULL) { - result = nk_true; + // Directories first, then files (two passes to group them). + for (int pass = 0; pass < 2; pass++) { + nk_bool want_dir = (pass == 0) ? nk_true : nk_false; + for (int i = 0; i < (int)filePathList.count; i++) { + nk_bool is_dir = DirectoryExists(filePathList.paths[i]) ? nk_true : nk_false; + if (is_dir != want_dir) { + continue; } - } - } - - // Files - for (int i = 0; i < filePathList.count; i++) { - if (FileExists(filePathList.paths[i]) && !DirectoryExists(filePathList.paths[i])) { - if (nk_console_file_add_entry(console, filePathList.paths[i], nk_false) != NULL) { + if (nk_console_file_add_entry(console, filePathList.paths[i], is_dir) != NULL) { result = nk_true; } } diff --git a/nuklear_console_image.h b/nuklear_console_image.h index 745b894..a070189 100644 --- a/nuklear_console_image.h +++ b/nuklear_console_image.h @@ -93,7 +93,7 @@ NK_API struct nk_rect nk_console_image_render(nk_console* widget) { } if (nk_console_is_active_widget(widget)) { - nk_console_check_up_down(widget, widget_bounds); + nk_console_check_up_down(widget); nk_console_check_tooltip(widget); } diff --git a/nuklear_console_label.h b/nuklear_console_label.h index ffce617..f8ca100 100644 --- a/nuklear_console_label.h +++ b/nuklear_console_label.h @@ -61,7 +61,7 @@ NK_API struct nk_rect nk_console_label_render(nk_console* widget) { nk_console_trigger_event(widget, NK_CONSOLE_EVENT_CLICKED); } else { - nk_console_check_up_down(widget, widget_bounds); + nk_console_check_up_down(widget); } nk_console_check_tooltip(widget); } diff --git a/nuklear_console_list_view.h b/nuklear_console_list_view.h index 93b1c4f..dc6239b 100644 --- a/nuklear_console_list_view.h +++ b/nuklear_console_list_view.h @@ -79,6 +79,9 @@ NK_API nk_flags nk_console_list_view_flags(nk_console* list_view); */ NK_API void nk_console_list_view_set_flags(nk_console* list_view, nk_flags flags); +NK_API void nk_console_list_view_set_item_count(nk_console* list_view, nk_uint item_count); +NK_API nk_uint nk_console_list_view_item_count(nk_console* list_view); + #if defined(__cplusplus) } #endif @@ -89,8 +92,7 @@ NK_API void nk_console_list_view_set_flags(nk_console* list_view, nk_flags flags #ifndef NK_CONSOLE_LIST_VIEW_IMPLEMENTATION_ONCE #define NK_CONSOLE_LIST_VIEW_IMPLEMENTATION_ONCE - -NK_API nk_flags nk_console_list_view_item_count(nk_console* list_view) { +NK_API nk_uint nk_console_list_view_item_count(nk_console* list_view) { if (list_view == NULL || list_view->data == NULL || list_view->type != NK_CONSOLE_LIST_VIEW) { return 0; } @@ -104,6 +106,11 @@ NK_API void nk_console_list_view_set_item_count(nk_console* list_view, nk_uint i } nk_console_list_view_data* data = (nk_console_list_view_data*)list_view->data; data->row_count = item_count; + data->selected = 0; + data->_scroll_y = 0; + if (data->view.scroll_pointer) { + *data->view.scroll_pointer = 0; + } } NK_API nk_flags nk_console_list_view_flags(nk_console* list_view) { @@ -189,19 +196,19 @@ NK_API struct nk_rect nk_console_list_view_render(nk_console* widget) { top_data->up_down_repeat_timer = 0; } - if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_LB)) { + if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_LB) || nk_input_is_key_pressed(&top->ctx->input, NK_KEY_SCROLL_UP)) { // Page up: jump selection up by rows_visible items. if (data->selected > 0) { - data->selected = NK_MAX(0, data->selected - data->rows_visible); + data->selected = (nk_uint)NK_MAX(0, (int)data->selected - (int)data->rows_visible); if (data->view.scroll_pointer) { - nk_uint new_scroll = (nk_uint)data->selected * (nk_uint)scroll_row_height; + nk_uint new_scroll = data->selected * (nk_uint)scroll_row_height; *data->view.scroll_pointer = new_scroll; data->_scroll_y = new_scroll; } } top_data->input_processed = nk_true; } - else if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_RB)) { + else if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_RB) || nk_input_is_key_pressed(&top->ctx->input, NK_KEY_SCROLL_DOWN)) { // Page down: jump selection down by rows_visible items. if (data->row_count > 0 && data->selected < data->row_count - 1) { data->selected = NK_MIN(data->row_count - 1, data->selected + data->rows_visible); @@ -236,7 +243,7 @@ NK_API struct nk_rect nk_console_list_view_render(nk_console* widget) { nk_console* t = widget->parent->children[idx]; if (nk_console_selectable(t)) { nk_console_set_active_widget(t); - top_data->scroll_requested = nk_true; + top_data->scroll_to_widget = t; break; } } @@ -270,7 +277,7 @@ NK_API struct nk_rect nk_console_list_view_render(nk_console* widget) { nk_console* t = widget->parent->children[idx]; if (nk_console_selectable(t)) { nk_console_set_active_widget(t); - top_data->scroll_requested = nk_true; + top_data->scroll_to_widget = t; break; } } @@ -284,7 +291,7 @@ NK_API struct nk_rect nk_console_list_view_render(nk_console* widget) { nk_console* t = widget->parent->children[idx]; if (nk_console_selectable(t)) { nk_console_set_active_widget(t); - top_data->scroll_requested = nk_true; + top_data->scroll_to_widget = t; break; } } @@ -298,7 +305,7 @@ NK_API struct nk_rect nk_console_list_view_render(nk_console* widget) { nk_console* t = widget->parent->children[idx]; if (nk_console_selectable(t)) { nk_console_set_active_widget(t); - top_data->scroll_requested = nk_true; + top_data->scroll_to_widget = t; break; } } @@ -311,7 +318,6 @@ NK_API struct nk_rect nk_console_list_view_render(nk_console* widget) { else if (nk_console_button_pushed(top, NK_GAMEPAD_BUTTON_B)) { if (widget->parent != NULL) { nk_console_navigate_back(widget->parent); - top_data->scroll_requested = nk_true; } top_data->input_processed = nk_true; } diff --git a/nuklear_console_message.h b/nuklear_console_message.h index fc7cce7..2e47bed 100644 --- a/nuklear_console_message.h +++ b/nuklear_console_message.h @@ -121,7 +121,7 @@ NK_API void nk_console_message_render(nk_console* console, nk_console_message* m NK_API void nk_console_render_message(nk_console* console) { nk_console_top_data* data = (nk_console_top_data*)console->data; - if (data->messages == NULL || cvector_size(data->messages) == 0) { + if (data->messages == NULL || cvector_empty(data->messages)) { return; } diff --git a/nuklear_console_progress.h b/nuklear_console_progress.h index dbb708e..1c4edfd 100644 --- a/nuklear_console_progress.h +++ b/nuklear_console_progress.h @@ -109,7 +109,7 @@ NK_API struct nk_rect nk_console_progress_render(nk_console* console) { console->ctx->style.progress.cursor_normal = cursor_active; } else { - console->ctx->style.progress.cursor_normal = cursor_active; + console->ctx->style.progress.cursor_normal = cursor_hover; } } @@ -129,7 +129,7 @@ NK_API struct nk_rect nk_console_progress_render(nk_console* console) { // Allow switching up/down in widgets if (nk_console_is_active_widget(console)) { - nk_console_check_up_down(console, widget_bounds); + nk_console_check_up_down(console); nk_console_check_tooltip(console); } diff --git a/nuklear_console_property.h b/nuklear_console_property.h index 7748ed1..a346707 100644 --- a/nuklear_console_property.h +++ b/nuklear_console_property.h @@ -171,16 +171,26 @@ NK_API struct nk_rect nk_console_property_render(nk_console* console) { switch (console->type) { case NK_CONSOLE_PROPERTY_INT: { char name[NK_MAX_NUMBER_BUFFER]; - NK_MEMCPY(name + 2, console->label, (nk_size)(nk_strlen(console->label) + 1)); + int label_len = nk_strlen(console->label); + if (label_len > NK_MAX_NUMBER_BUFFER - 3) { + label_len = NK_MAX_NUMBER_BUFFER - 3; + } name[0] = '#'; name[1] = '#'; + NK_MEMCPY(name + 2, console->label, (nk_size)label_len); + name[label_len + 2] = '\0'; nk_property_int(console->ctx, name, data->min_int, data->val_int, data->max_int, data->step_int, data->inc_per_pixel); } break; case NK_CONSOLE_PROPERTY_FLOAT: { char name[NK_MAX_NUMBER_BUFFER]; - NK_MEMCPY(name + 2, console->label, (nk_size)(nk_strlen(console->label) + 1)); + int label_len = nk_strlen(console->label); + if (label_len > NK_MAX_NUMBER_BUFFER - 3) { + label_len = NK_MAX_NUMBER_BUFFER - 3; + } name[0] = '#'; name[1] = '#'; + NK_MEMCPY(name + 2, console->label, (nk_size)label_len); + name[label_len + 2] = '\0'; nk_property_float(console->ctx, name, data->min_float, data->val_float, data->max_float, data->step_float, data->inc_per_pixel); } break; case NK_CONSOLE_SLIDER_INT: @@ -226,7 +236,7 @@ NK_API struct nk_rect nk_console_property_render(nk_console* console) { // Allow switching up/down in widgets if (nk_console_is_active_widget(console)) { - nk_console_check_up_down(console, widget_bounds); + nk_console_check_up_down(console); nk_console_check_tooltip(console); } diff --git a/nuklear_console_radio.h b/nuklear_console_radio.h index f6d07c6..52ebae6 100644 --- a/nuklear_console_radio.h +++ b/nuklear_console_radio.h @@ -212,7 +212,7 @@ NK_API struct nk_rect nk_console_radio_render(nk_console* widget) { // Allow switching up/down in widgets if (active) { - nk_console_check_up_down(widget, widget_bounds); + nk_console_check_up_down(widget); nk_console_check_tooltip(widget); } diff --git a/nuklear_console_row.h b/nuklear_console_row.h index b6fd225..3cbb6a4 100644 --- a/nuklear_console_row.h +++ b/nuklear_console_row.h @@ -224,7 +224,7 @@ NK_API struct nk_rect nk_console_row_render(nk_console* console) { // Consume directional input before children have a chance to. if (nk_console_is_active_widget(console)) { nk_console_row_check_left_right(console, top); - nk_console_check_up_down(console, widget_bounds); + nk_console_check_up_down(console); nk_console* active = nk_console_get_active_widget(console); // Attempt to accurately move vertically if the new widget is also a row. diff --git a/nuklear_console_textedit.h b/nuklear_console_textedit.h index 4f00a44..7254ad3 100644 --- a/nuklear_console_textedit.h +++ b/nuklear_console_textedit.h @@ -218,7 +218,9 @@ NK_API void nk_console_textedit_key_click(nk_console* key, void* user_data) { case NK_SYMBOL_TRIANGLE_LEFT: { int len = nk_strlen(data->buffer); if (len > 0) { - data->buffer[len - 1] = '\0'; + // Walk back past UTF-8 continuation bytes (0x80-0xBF) to erase the full codepoint. + do { len--; } while (len > 0 && ((unsigned char)data->buffer[len] & 0xC0) == 0x80); + data->buffer[len] = '\0'; nk_console_trigger_event(textedit, NK_CONSOLE_EVENT_CHANGED); } } break; @@ -235,11 +237,11 @@ NK_API void nk_console_textedit_key_click(nk_console* key, void* user_data) { // Any key character case NK_SYMBOL_NONE: { - // Add the character to the buffer. + // Append all bytes of the key label to support multi-byte UTF-8 glyphs. int len = nk_strlen(data->buffer); - if (len < data->buffer_size - 1) { - data->buffer[len] = key->label[0]; - data->buffer[len + 1] = '\0'; + int key_len = nk_strlen(key->label); + if (len + key_len < data->buffer_size) { + NK_MEMCPY(data->buffer + len, key->label, (nk_size)(key_len + 1)); nk_console_trigger_event(textedit, NK_CONSOLE_EVENT_CHANGED); } } break; @@ -270,7 +272,7 @@ NK_API void nk_console_textedit_button_main_click(nk_console* button, void* user // Create the textedit_text widget, which is the input box. nk_console_textedit_text(button); - // TODO: Add option for UTF-8 keys with nk_glyph. + // TODO: Add non-ASCII keyboard layouts (e.g. accented characters, CJK) using nk_glyph key labels. // First row: 1 - 0 nk_console* row = nk_console_row_begin(button); diff --git a/nuklear_console_textedit_text.h b/nuklear_console_textedit_text.h index 54a2a04..4ca40a6 100644 --- a/nuklear_console_textedit_text.h +++ b/nuklear_console_textedit_text.h @@ -51,7 +51,7 @@ NK_API struct nk_rect nk_console_textedit_text_render(nk_console* widget) { } // Allow changing up/down only if they're not pressing backspace else if (!nk_input_is_key_pressed(&widget->ctx->input, NK_KEY_BACKSPACE)) { - nk_console_check_up_down(widget, widget_bounds); + nk_console_check_up_down(widget); } // Display the tooltip for the textedit. @@ -85,7 +85,7 @@ NK_API struct nk_rect nk_console_textedit_text_render(nk_console* widget) { int mask_strlen = (int)nk_strlen(mask); if (mask_strlen < buffer_strlen) { // Characters deleted int real_len = (int)nk_strlen(data->buffer); - int new_real_len = real_len - buffer_strlen - mask_strlen; + int new_real_len = real_len - (buffer_strlen - mask_strlen); if (new_real_len < 0) { new_real_len = 0; } diff --git a/nuklear_console_tree.h b/nuklear_console_tree.h index 2eda1e9..c42bc4e 100644 --- a/nuklear_console_tree.h +++ b/nuklear_console_tree.h @@ -82,7 +82,7 @@ static void nk_console_tree_apply_expanded(nk_console* tree, nk_bool expanded) { nk_console* top = nk_console_get_top(tree); if (top != NULL && top->data != NULL) { nk_console_top_data* top_data = (nk_console_top_data*)top->data; - top_data->scroll_requested = nk_true; + top_data->scroll_to_widget = tree; top_data->scrollbar_required = nk_true; } }