#include #include #include #include #include #include #include #include #include // TODO: Set window dimensions and font size based on pixel area density. static const char* WindowTitle = "XPLORER"; static const int DefaultWidth = 720; static const int DefaultHeight = 450; // #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 MouseCoordsToUiCoords( SDL_Window*, float mouse_x, float mouse_y, int* x, int* y); uiMouseButtonState MouseButtonStateFromFlags( SDL_MouseButtonFlags flags, uiMouseButton 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 char* text = uiTableGet(table, event->row, event->col); printf("Click: %d,%d: %s\n", event->row, event->col, text); // TODO: Handle '.' and '..' better. Define a path concatenation function. path child_dir = path_new(text); 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)) { 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, DefaultWidth, DefaultHeight, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY)) == 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 | SDL_INIT_EVENTS)) { success = false; goto cleanup; } if (!uiInit()) { success = false; goto cleanup; } 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; // Scrolling on a trackpad often results in scroll values in [0,1], but the UI // library handles integer scroll deltas only. // Accumulate floating-point deltas and issue them to the UI library when // the integer part becomes non-zero. float scroll = 0.f; 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)) { success = false; break; } if ((event.type == SDL_EVENT_QUIT) || (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)) { break; } // 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.type == SDL_EVENT_WINDOW_DISPLAY_CHANGED) || (event.window.type == SDL_EVENT_WINDOW_RESIZED) || (event.window.type == SDL_EVENT_WINDOW_MOVED)) { if (!Resize(&state)) { success = false; break; } redraw = true; } else if (event.type == SDL_EVENT_KEY_DOWN) { if (event.key.mod & SDL_KMOD_LCTRL) { switch (event.key.key) { // Exit. case SDLK_C: case SDLK_D: running = false; break; default: break; } } } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { int x, y; MouseCoordsToUiCoords( state.window, event.button.x, event.button.y, &x, &y); const uiInputEvent ui_event = { .type = uiEventMouseButton, .mouse_button = (uiMouseButtonEvent){.button = ToUiButton(event.button.button), .button_state = uiMouseDown, .mouse_position = (uiPoint){x, y}} }; redraw = uiSendEvent(state.frame, &ui_event); } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { int x, y; MouseCoordsToUiCoords( state.window, event.button.x, event.button.y, &x, &y); const uiInputEvent ev = { .type = uiEventMouseButton, .mouse_button = (uiMouseButtonEvent){.button = ToUiButton(event.button.button), .button_state = uiMouseUp, .mouse_position = (uiPoint){x, y}} }; redraw = uiSendEvent(state.frame, &ev); } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { scroll += event.wheel.y; const int scroll_int = (int)scroll; scroll = scroll - (float)scroll_int; if (scroll_int != 0) { int x, y; MouseCoordsToUiCoords( state.window, event.wheel.mouse_x, event.wheel.mouse_y, &x, &y); const uiInputEvent ev = { .type = uiEventMouseScroll, .mouse_scroll = (uiMouseScrollEvent){.scroll_offset = scroll_int, .mouse_position = (uiPoint){x, y}} }; redraw = uiSendEvent(state.frame, &ev); } } else if (event.type == SDL_EVENT_MOUSE_MOTION) { int x, y; MouseCoordsToUiCoords( state.window, event.motion.x, event.motion.y, &x, &y); const uiInputEvent ev = { .type = uiEventMouseMove, .mouse_move = (uiMouseMoveEvent){ .button_state = { MouseButtonStateFromFlags(event.motion.state, uiLMB), MouseButtonStateFromFlags(event.motion.state, uiRMB), }, .mouse_position = (uiPoint){x, y}} }; 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 = nullptr; } uiShutdown(); SDL_Quit(); return success ? 0 : 1; } // ----------------------------------------------------------------------------- uiMouseButton ToUiButton(Uint8 button) { // TODO: Buttons. return uiLMB; } void MouseCoordsToUiCoords( SDL_Window* window, float mouse_x, float mouse_y, int* x, int* y) { assert(window); assert(x); assert(y); const float d = SDL_GetWindowPixelDensity(window); *x = (int)(mouse_x * d); *y = (int)(mouse_y * d); } uiMouseButtonState MouseButtonStateFromFlags( SDL_MouseButtonFlags flags, uiMouseButton button) { switch (button) { case uiLMB: return (flags & SDL_BUTTON_LMASK) ? uiMouseDown : uiMouseUp; case uiRMB: return (flags & SDL_BUTTON_RMASK) ? uiMouseDown : uiMouseUp; default: assert(false); return uiMouseUp; } }