#include #include #include #include #include #include #include #include #include static const char* WindowTitle = "XPLORER"; static const int DefaultWidth = 1440; static const int DefaultHeight = 900; // #define DEBUG_EVENT_LOOP 1 #ifdef DEBUG_EVENT_LOOP #define EVENT_LOOP_PRINT printf #else #define EVENT_LOOP_PRINT(...) #endif // DEBUG_EVENT_LOOP typedef struct State { SDL_Window* window; uiFrame* frame; uiTable* table; path current_dir; } State; uiMouseButton ToUiButton(Uint8 button); void CreateUi(State* state) { assert(state); uiFrame* frame = uiMakeFrame(); const char* header[] = {"Name", "Size", "Modified"}; uiTable* table = uiMakeTable(0, sizeof(header) / sizeof(char*), header); assert(table); uiWidgetSetParent(uiMakeTablePtr(table), uiMakeFramePtr(frame)); // uiLabel* label = uiMakeLabel("Hello world, what is going on!?"); // uiWidgetSetParent(label, frame); state->frame = frame; state->table = table; } int compare_files(const void* _a, const void* _b) { assert(_a); assert(_b); const tinydir_file* a = _a; const tinydir_file* b = _b; for (size_t i = 0; (i < _TINYDIR_FILENAME_MAX) && (a->name[i] != 0) && (b->name[i] != 0); ++i) { if (a->name[i] < b->name[i]) { return -1; } else if (a->name[i] > b->name[i]) { return 1; } } return 0; } size_t GetFileCount(path directory) { size_t count = 0; tinydir_dir dir; if (tinydir_open(&dir, path_cstr(directory)) == 0) { for (count = 0; dir.has_next; ++count) { tinydir_next(&dir); } } return count; } bool SetDirectory(State* state, path directory) { assert(state); bool directory_changed = false; tinydir_dir dir; if (tinydir_open(&dir, path_cstr(directory)) == 0) { const size_t count = GetFileCount(directory); tinydir_file* files = calloc(count, sizeof(tinydir_file)); if (!files) { return false; } for (size_t i = 0; dir.has_next; ++i) { assert(i < count); tinydir_readfile(&dir, &files[i]); tinydir_next(&dir); } qsort(files, count, sizeof(files[0]), compare_files); uiTable* table = state->table; assert(table); uiTableClear(table); for (size_t i = 0; i < count; ++i) { tinydir_file file = files[i]; const string file_size = string_format_size(file._s.st_size); const char* row[3] = {file.name, string_data(file_size), ""}; uiTableAddRow(table, row); } free(files); if (!path_empty(state->current_dir)) { path_del(&state->current_dir); } state->current_dir = directory; directory_changed = true; } return directory_changed; } bool OnFileTableClick( State* state, uiTable* table, const uiTableClickEvent* event) { assert(state); assert(table); if (event->col == 0) { // Clicked the file/directory name. // TODO: Think more about uiPtr. Do we need uiConstPtr? // Ideally: const uiLabel* label = uiGetPtr(uiTableGet(...)); // i.e., no checks on the client code; all checks in library code. const uiLabel* label = (const uiLabel*)uiTableGet(table, event->row, event->col); assert(uiWidgetGetType((const uiWidget*)label) == uiTypeLabel); printf("Click: %d,%d: %s\n", event->row, event->col, uiLabelGetText(label)); // TODO: Handle '.' and '..' better. Define a path concatenation function. path child_dir = path_new(uiLabelGetText(label)); path new_dir = path_concat(state->current_dir, child_dir); const bool result = SetDirectory(state, new_dir); if (!result) { path_del(&new_dir); } path_del(&child_dir); return result; } return false; } /// Handle widget events and return whether a redraw is needed. bool HandleWidgetEvents(State* state) { assert(state); bool redraw = false; const uiWidgetEvent* events; const int numWidgetEvents = uiGetEvents(&events); for (int i = 0; i < numWidgetEvents; ++i) { const uiWidgetEvent* ev = &events[i]; // TODO: Set and check widget IDs. switch (ev->type) { case uiWidgetEventClick: if (ev->widget.type == uiTypeTable) { if (OnFileTableClick( state, uiGetTablePtr(ev->widget), &ev->table_click)) { redraw = true; } } break; default: break; } } return redraw; } static bool Render(State* state) { assert(state); assert(state->window); SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); assert(window_surface); #ifdef DEBUG_EVENT_LOOP const uiSize frame_size = uiGetFrameSize(state->frame); EVENT_LOOP_PRINT( "Render; surface: %dx%d; window surface; %dx%d\n", frame_size.width, frame_size.height, window_surface->w, window_surface->h); #endif // Locking/unlocking SDL software surfaces is not necessary. // // Probably also best to avoid SDL_BlitSurface(); it does pixel format // conversion while blitting one pixel at a time. Instead, make the UI pixel // format match the SDL window's and write to SDL's back buffer directly. uiRender( state->frame, &(uiSurface){ .width = window_surface->w, .height = window_surface->h, .pixels = window_surface->pixels, }); if (SDL_UpdateWindowSurface(state->window) != 0) { return false; } return true; } static bool Resize(State* state) { assert(state); // int width, height; // SDL_GetWindowSize(state->window, &width, &height); const SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); if (!window_surface) { return false; } const int width = window_surface->w; const int height = window_surface->h; EVENT_LOOP_PRINT("Resize: %dx%d\n", width, height); // TODO: Fix the white 1-pixel vertical/horizontal line that appears at odd // sizes when resizing the window. // https://github.com/libsdl-org/SDL/issues/9653 uiResizeFrame(state->frame, width, height); return true; } bool Initialize(State* state) { assert(state); if ((state->window = SDL_CreateWindow( WindowTitle, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, DefaultWidth, DefaultHeight, SDL_WINDOW_SHOWN | SDL_WINDOW_RESIZABLE)) == NULL) { return false; } CreateUi(state); path home = path_new(getenv("HOME")); SetDirectory(state, home); return true; } int main( __attribute__((unused)) int argc, __attribute__((unused)) const char** argv) { bool success = true; State state = {0}; if (SDL_Init(SDL_INIT_VIDEO) != 0) { return false; } if (!uiInit()) { return false; } if (!Initialize(&state)) { success = false; goto cleanup; } if (!Resize(&state)) { success = false; goto cleanup; } // TODO: All of the window and input handling could be moved to its own // library so that different applications can re-use it. // Controls whether we should keep running. bool running = true; // Controls whether a redraw is required. // Initially true to perform an initial draw before the window is displayed. bool redraw = true; while (running) { EVENT_LOOP_PRINT("loop\n"); // Draw if needed. if (redraw && !Render(&state)) { success = false; break; } redraw = false; // Handle events. SDL_Event event = {0}; if (SDL_WaitEvent(&event) == 0) { success = false; break; } else if (event.type == SDL_QUIT) { break; } else { if (event.type == SDL_WINDOWEVENT) { // When the window is maximized, an SDL_WINDOWEVENT_MOVED comes in // before an SDL_WINDOWEVENT_SIZE_CHANGED with the window already // resized. This is unfortunate because we cannot rely on the latter // event alone to handle resizing. if ((event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) || (event.window.event == SDL_WINDOWEVENT_RESIZED) || (event.window.event == SDL_WINDOWEVENT_MOVED)) { if (!Resize(&state)) { success = false; break; } redraw = true; } } else if (event.type == SDL_KEYDOWN) { if (event.key.keysym.mod & KMOD_LCTRL) { switch (event.key.keysym.sym) { // Exit. case SDLK_c: case SDLK_d: running = false; break; default: break; } } } else if (event.type == SDL_MOUSEBUTTONDOWN) { const uiInputEvent ev = { .type = uiEventMouseButton, .mouse_button = (uiMouseButtonEvent){ .button = ToUiButton(event.button.button), .state = uiMouseDown, .mouse_position = (uiPoint){.x = event.button.x, .y = event.button.y}} }; redraw = uiSendEvent(state.frame, &ev); } else if (event.type == SDL_MOUSEBUTTONUP) { const uiInputEvent ev = { .type = uiEventMouseButton, .mouse_button = (uiMouseButtonEvent){ .button = ToUiButton(event.button.button), .state = uiMouseUp, .mouse_position = (uiPoint){.x = event.button.x, .y = event.button.y}} }; redraw = uiSendEvent(state.frame, &ev); } else if (event.type == SDL_MOUSEWHEEL) { const uiInputEvent ev = { .type = uiEventMouseScroll, .mouse_scroll = (uiMouseScrollEvent){ .scroll_offset = event.wheel.y, .mouse_position = (uiPoint){ .x = event.wheel.mouseX, .y = event.wheel.mouseY}} }; redraw = uiSendEvent(state.frame, &ev); } else { EVENT_LOOP_PRINT("event.window.event = %d\n", event.window.event); } if (HandleWidgetEvents(&state)) { Resize(&state); // Trigger a re-layout of widgets. redraw = true; } } } cleanup: if (!success) { fprintf(stderr, "%s\n", SDL_GetError()); } if (state.frame) { uiDestroyFrame(&state.frame); } if (state.window) { SDL_DestroyWindow(state.window); state.window = 0; } uiShutdown(); SDL_Quit(); return success ? 0 : 1; } // ----------------------------------------------------------------------------- uiMouseButton ToUiButton(Uint8 button) { // TODO: Buttons. return uiLMB; }