From acaa801ea2c7fa3c909ca4f64759696fff002b1a Mon Sep 17 00:00:00 2001
From: 3gg <3gg@shellblade.net>
Date: Sat, 22 Jun 2024 13:21:09 -0700
Subject: Basic navigation.

---
 CMakeLists.txt |   1 +
 src/xplorer.c  | 215 +++++++++++++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 188 insertions(+), 28 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 85e1921..655eb9e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,6 +20,7 @@ target_include_directories(xplorer PRIVATE
 
 target_link_libraries(xplorer PRIVATE
   SDL2-static
+  filesystem
   tinydir
   ui)
 
diff --git a/src/xplorer.c b/src/xplorer.c
index 8a190af..5e69d82 100644
--- a/src/xplorer.c
+++ b/src/xplorer.c
@@ -5,14 +5,14 @@
 #include <tinydir.h>
 
 #include <assert.h>
+#include <path.h>
 #include <stdbool.h>
 #include <stdio.h>
 #include <stdlib.h>
-#include <string.h>
 
 static const char* WindowTitle   = "XPLORER";
-static const int   DefaultWidth  = 960;
-static const int   DefaultHeight = 600;
+static const int   DefaultWidth  = 1440;
+static const int   DefaultHeight = 900;
 
 // #define DEBUG_EVENT_LOOP 1
 
@@ -26,47 +26,162 @@ typedef struct State {
   SDL_Window* window;
   uiFrame*    frame;
   uiTable*    table;
-  string      current_dir;
+  path        current_dir;
 } State;
 
-void SetDirectory(State* state, string path) {
+uiMouseButton ToUiButton(Uint8 button);
+
+void CreateUi(State* state) {
   assert(state);
 
-  state->current_dir = path;
+  uiFrame* frame = uiMakeFrame();
 
-  uiTable* table = state->table;
+  const char* header[] = {"Name", "Size", "Modified"};
+  uiTable*    table    = uiMakeTable(0, sizeof(header) / sizeof(char*), header);
   assert(table);
+  uiWidgetSetParent(uiMakeTablePtr(table), uiMakeFramePtr(frame));
 
-  tinydir_dir dir;
-  tinydir_open(&dir, string_data(path));
-  while (dir.has_next) {
-    tinydir_file file;
-    tinydir_readfile(&dir, &file);
+  // uiLabel* label = uiMakeLabel("Hello world, what is going on!?");
+  // uiWidgetSetParent(label, frame);
 
-    const string file_size = string_format_size(file._s.st_size);
+  state->frame = frame;
+  state->table = table;
+}
 
-    const char* row[3] = {file.name, string_data(file_size), "<date>"};
-    uiTableAddRow(table, row);
+int compare_files(const void* _a, const void* _b) {
+  assert(_a);
+  assert(_b);
 
-    tinydir_next(&dir);
+  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;
 }
 
-void CreateUi(State* state) {
+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);
 
-  uiFrame* frame = uiMakeFrame();
+  bool directory_changed = false;
 
-  const char* header[] = {"Name", "Size", "Modified"};
-  uiTable*    table    = uiMakeTable(0, sizeof(header) / sizeof(char*), header);
+  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), "<date>"};
+      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);
-  uiWidgetSetParent(uiMakeTablePtr(table), uiMakeFramePtr(frame));
 
-  // uiLabel* label = uiMakeLabel("Hello world, what is going on!?");
-  // uiWidgetSetParent(label, frame);
+  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;
+}
 
-  state->frame = frame;
-  state->table = table;
+/// 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) {
@@ -119,6 +234,7 @@ static bool Resize(State* state) {
 
   // 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;
@@ -136,8 +252,8 @@ bool Initialize(State* state) {
 
   CreateUi(state);
 
-  const char* home = getenv("HOME");
-  SetDirectory(state, string_new(home));
+  path home = path_new(getenv("HOME"));
+  SetDirectory(state, home);
 
   return true;
 }
@@ -167,6 +283,9 @@ int main(
     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;
 
@@ -218,10 +337,43 @@ int main(
             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;
+      }
     }
   }
 
@@ -243,3 +395,10 @@ cleanup:
 
   return success ? 0 : 1;
 }
+
+// -----------------------------------------------------------------------------
+
+uiMouseButton ToUiButton(Uint8 button) {
+  // TODO: Buttons.
+  return uiLMB;
+}
-- 
cgit v1.2.3