From 2f2d42e28a14cdc856f8cf0c45cd572646be6750 Mon Sep 17 00:00:00 2001
From: 3gg <3gg@shellblade.net>
Date: Sat, 22 Jun 2024 13:23:42 -0700
Subject: Table user input.

---
 include/ui.h | 124 ++++++++++++++++++++--
 src/ui.c     | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++--------
 2 files changed, 402 insertions(+), 51 deletions(-)

diff --git a/include/ui.h b/include/ui.h
index 43bb2e7..8570552 100644
--- a/include/ui.h
+++ b/include/ui.h
@@ -36,6 +36,9 @@ typedef struct uiPoint {
   int y;
 } uiPoint;
 
+/// Widget ID.
+typedef int uiWidgetId;
+
 /// Widget type.
 typedef enum uiWidgetType {
   uiTypeButton,
@@ -52,7 +55,7 @@ typedef struct uiTable  uiTable;
 typedef struct uiWidget uiWidget;
 
 /// Widget pointer.
-typedef struct uiWidgetPtr {
+typedef struct uiPtr {
   uiWidgetType type;
   union {
     uiButton* button;
@@ -61,7 +64,77 @@ typedef struct uiWidgetPtr {
     uiTable*  table;
     uiWidget* widget;
   };
-} uiWidgetPtr;
+} uiPtr;
+
+/// Mouse button.
+typedef enum uiMouseButton {
+  uiLMB,
+  uiRMB,
+  uiMouseButtonMax,
+} uiMouseButton;
+
+/// Mouse button state.
+typedef enum uiMouseButtonState {
+  uiMouseUp,
+  uiMouseDown,
+} uiMouseButtonState;
+
+/// Mouse button event.
+typedef struct uiMouseButtonEvent {
+  uiMouseButton      button;
+  uiMouseButtonState state;
+  uiPoint            mouse_position;
+} uiMouseButtonEvent;
+
+/// Mouse click event.
+typedef struct uiMouseClickEvent {
+  uiMouseButton button;
+  uiPoint       mouse_position;
+} uiMouseClickEvent;
+
+/// Mouse scroll event.
+typedef struct uiMouseScrollEvent {
+  uiPoint mouse_position;
+  int     scroll_offset; /// Positive = down; negative = up.
+} uiMouseScrollEvent;
+
+/// Input event type.
+typedef enum uiInputEventType {
+  uiEventMouseButton,
+  uiEventMouseClick,
+  uiEventMouseScroll,
+} uiInputEventType;
+
+/// Input event.
+typedef struct uiInputEvent {
+  uiInputEventType type;
+  union {
+    uiMouseButtonEvent mouse_button;
+    uiMouseClickEvent  mouse_click;
+    uiMouseScrollEvent mouse_scroll;
+  };
+} uiInputEvent;
+
+/// Table click event.
+typedef struct uiTableClickEvent {
+  int col;
+  int row;
+} uiTableClickEvent;
+
+/// UI event type.
+typedef enum uiWidgetEventType {
+  uiWidgetEventClick,
+} uiWidgetEventType;
+
+/// UI event.
+/// These are events from the UI widgets back to the client application.
+typedef struct uiWidgetEvent {
+  uiWidgetEventType type;
+  uiPtr             widget;
+  union {
+    uiTableClickEvent table_click;
+  };
+} uiWidgetEvent;
 
 // -----------------------------------------------------------------------------
 // Library.
@@ -74,15 +147,25 @@ bool uiInit(void);
 /// This should be called once during application shutdown.
 void uiShutdown(void);
 
+// -----------------------------------------------------------------------------
+// Widget pointers.
+
+uiPtr uiMakeButtonPtr(uiButton*);
+uiPtr uiMakeFramePtr(uiFrame*);
+uiPtr uiMakeLabelPtr(uiLabel*);
+uiPtr uiMakeTablePtr(uiTable*);
+
+uiButton* uiGetButtonPtr(uiPtr ptr);
+uiFrame*  uiGetFramePtr(uiPtr ptr);
+uiLabel*  uiGetLabelPtr(uiPtr ptr);
+uiTable*  uiGetTablePtr(uiPtr ptr);
+
 // -----------------------------------------------------------------------------
 // Widget.
 
-uiWidgetPtr uiMakeButtonPtr(uiButton*);
-uiWidgetPtr uiMakeFramePtr(uiFrame*);
-uiWidgetPtr uiMakeLabelPtr(uiLabel*);
-uiWidgetPtr uiMakeTablePtr(uiTable*);
+uiWidgetType uiWidgetGetType(const uiWidget*);
 
-void uiWidgetSetParent(uiWidgetPtr child, uiWidgetPtr parent);
+void uiWidgetSetParent(uiPtr child, uiPtr parent);
 
 // -----------------------------------------------------------------------------
 // Button.
@@ -111,17 +194,24 @@ uiSize uiGetFrameSize(const uiFrame*);
 /// Create a label.
 uiLabel* uiMakeLabel(const char* text);
 
+/// Return the label's text.
+const char* uiLabelGetText(const uiLabel*);
+
 // -----------------------------------------------------------------------------
 // Table.
 
 /// Create a table.
 uiTable* uiMakeTable(int rows, int cols, const char** header);
 
+/// Clear the table.
+/// This clears the contents, but not the header.
+void uiTableClear(uiTable*);
+
 /// Add a row.
 void uiTableAddRow(uiTable*, const char** row);
 
 /// Set the table's cell.
-void uiTableSet(uiTable*, int row, int col, uiWidgetPtr widget);
+void uiTableSet(uiTable*, int row, int col, uiPtr widget);
 
 /// Get the table's cell.
 const uiWidget* uiTableGet(const uiTable*, int row, int col);
@@ -134,3 +224,21 @@ uiWidget* uiTableGetMut(uiTable*, int row, int col);
 
 /// Render the frame.
 void uiRender(const uiFrame*, uiSurface*);
+
+// -----------------------------------------------------------------------------
+// UI Events.
+
+/// Get the widget events.
+/// Return the number of events in the returned array.
+///
+/// This function clears the events recorded by the UI library since the last
+/// input event. Subsequent calls to this function, with no further user input,
+/// therefore report zero widget events.
+int uiGetEvents(uiWidgetEvent const**);
+
+// -----------------------------------------------------------------------------
+// User input.
+
+/// Send an input event to the UI.
+/// Return true if the UI requires a redraw.
+bool uiSendEvent(uiFrame*, const uiInputEvent*);
diff --git a/src/ui.c b/src/ui.c
index a5ab8d3..e8c8ee2 100644
--- a/src/ui.c
+++ b/src/ui.c
@@ -7,6 +7,10 @@
 
 #include <stdlib.h>
 
+#define Max(a, b) ((a) > (b) ? (a) : (b))
+
+#define MaxWidgetEvents 8
+
 static void* uiAlloc(size_t count, size_t size) {
   void* mem = calloc(count, size);
   ASSERT(mem);
@@ -60,13 +64,17 @@ typedef struct uiTable {
   uiWidget widget;
   int      rows;
   int      cols;
-  int*     widths; /// Width, in pixels, for each each column.
-  uiCell*  header; /// If non-null, row of 'cols' header cells.
-  uiCell*  cells;  /// Array of 'rows * cols' cells.
+  int*     widths; // Width, in pixels, for each column.
+  uiCell*  header; // If non-null, row of 'cols' header cells.
+  uiCell** cells;  // Array of 'rows' rows, each of 'cols' cells.
+  int      offset; // Offset into the rows of the table. Units: rows.
 } uiTable;
 
 typedef struct uiLibrary {
-  FontAtlas* font;
+  FontAtlas*         font;
+  uiMouseButtonState mouse_button_state[uiMouseButtonMax];
+  uiWidgetEvent      widget_events[MaxWidgetEvents];
+  int                num_widget_events;
 } uiLibrary;
 
 // -----------------------------------------------------------------------------
@@ -99,32 +107,65 @@ bool uiInit(void) {
 void uiShutdown(void) {}
 
 // -----------------------------------------------------------------------------
-// Widget.
+// Widget pointers.
+
+uiPtr uiMakeButtonPtr(uiButton* button) {
+  assert(button);
+  return (uiPtr){.type = uiTypeButton, .button = button};
+}
+
+uiPtr uiMakeFramePtr(uiFrame* frame) {
+  assert(frame);
+  return (uiPtr){.type = uiTypeFrame, .frame = frame};
+}
+
+uiPtr uiMakeLabelPtr(uiLabel* label) {
+  assert(label);
+  return (uiPtr){.type = uiTypeLabel, .label = label};
+}
+
+uiPtr uiMakeTablePtr(uiTable* table) {
+  assert(table);
+  return (uiPtr){.type = uiTypeTable, .table = table};
+}
+
+static uiPtr uiMakeWidgetPtr(uiWidget* widget) {
+  assert(widget);
+  return (uiPtr){.type = widget->type, .widget = widget};
+}
 
-static uiButton* uiGetButtonPtr(uiWidgetPtr ptr) {
+uiButton* uiGetButtonPtr(uiPtr ptr) {
   assert(ptr.type == uiTypeButton);
   assert(ptr.button);
   return ptr.button;
 }
 
-static uiFrame* uiGetFramePtr(uiWidgetPtr ptr) {
+uiFrame* uiGetFramePtr(uiPtr ptr) {
   assert(ptr.type == uiTypeFrame);
   assert(ptr.frame);
   return ptr.frame;
 }
 
-static uiLabel* uiGetLabelPtr(uiWidgetPtr ptr) {
+uiLabel* uiGetLabelPtr(uiPtr ptr) {
   assert(ptr.type == uiTypeLabel);
   assert(ptr.label);
   return ptr.label;
 }
 
-static uiTable* uiGetTablePtr(uiWidgetPtr ptr) {
+uiTable* uiGetTablePtr(uiPtr ptr) {
   assert(ptr.type == uiTypeTable);
   assert(ptr.table);
   return ptr.table;
 }
 
+// -----------------------------------------------------------------------------
+// Widget.
+
+uiWidgetType uiWidgetGetType(const uiWidget* widget) {
+  assert(widget);
+  return widget->type;
+}
+
 static void DestroyWidget(uiWidget** ppWidget) {
   assert(ppWidget);
 
@@ -135,27 +176,7 @@ static void DestroyWidget(uiWidget** ppWidget) {
   UI_DEL(ppWidget);
 }
 
-uiWidgetPtr uiMakeButtonPtr(uiButton* button) {
-  assert(button);
-  return (uiWidgetPtr){.type = uiTypeButton, .button = button};
-}
-
-uiWidgetPtr uiMakeFramePtr(uiFrame* frame) {
-  assert(frame);
-  return (uiWidgetPtr){.type = uiTypeFrame, .frame = frame};
-}
-
-uiWidgetPtr uiMakeLabelPtr(uiLabel* label) {
-  assert(label);
-  return (uiWidgetPtr){.type = uiTypeLabel, .label = label};
-}
-
-uiWidgetPtr uiMakeTablePtr(uiTable* table) {
-  assert(table);
-  return (uiWidgetPtr){.type = uiTypeTable, .table = table};
-}
-
-void uiWidgetSetParent(uiWidgetPtr child_, uiWidgetPtr parent_) {
+void uiWidgetSetParent(uiPtr child_, uiPtr parent_) {
   uiWidget* child  = child_.widget;
   uiWidget* parent = parent_.widget;
 
@@ -196,12 +217,21 @@ uiLabel* uiMakeLabel(const char* text) {
       .widget =
           (uiWidget){
                      .type = uiTypeLabel,
-                     },
+                     .rect =
+                  (uiRect){
+                      .width =
+                          (int)strlen(text) * g_ui.font->header.glyph_width,
+                      .height = g_ui.font->header.glyph_height}},
       .text = string_new(text),
   };
   return label;
 }
 
+const char* uiLabelGetText(const uiLabel* label) {
+  assert(label);
+  return string_data(label->text);
+}
+
 // -----------------------------------------------------------------------------
 // Frame.
 
@@ -226,7 +256,7 @@ uiSize uiGetFrameSize(const uiFrame* frame) {
 
 static const uiCell* GetCell(const uiTable* table, int row, int col) {
   assert(table);
-  return table->cells + (row * table->cols) + col;
+  return &table->cells[row][col];
 }
 
 static uiCell* GetCellMut(uiTable* table, int row, int col) {
@@ -234,10 +264,10 @@ static uiCell* GetCellMut(uiTable* table, int row, int col) {
   return (uiCell*)GetCell(table, row, col);
 }
 
-static uiCell* GetLastRow(uiTable* table) {
+static uiCell** GetLastRow(uiTable* table) {
   assert(table);
   assert(table->rows > 0);
-  return &table->cells[table->cols * (table->rows - 1)];
+  return &table->cells[table->rows - 1];
 }
 
 uiTable* uiMakeTable(int rows, int cols, const char** header) {
@@ -249,7 +279,7 @@ uiTable* uiMakeTable(int rows, int cols, const char** header) {
       .cols   = cols,
       .widths = (cols > 0) ? calloc(cols, sizeof(int)) : 0,
       .header = header ? calloc(cols, sizeof(uiCell)) : 0,
-      .cells  = (rows * cols > 0) ? calloc(rows * cols, sizeof(uiCell)) : 0,
+      .cells  = (rows * cols > 0) ? calloc(rows, sizeof(uiCell*)) : 0,
   };
 
   if (header) {
@@ -261,23 +291,50 @@ uiTable* uiMakeTable(int rows, int cols, const char** header) {
   return table;
 }
 
+void uiTableClear(uiTable* table) {
+  assert(table);
+
+  // Free row data.
+  if (table->cells) {
+    for (int row = 0; row < table->rows; ++row) {
+      for (int col = 0; col < table->cols; ++col) {
+        DestroyWidget(&table->cells[row][col].child);
+      }
+      free(table->cells[row]);
+    }
+    free(table->cells);
+    table->cells = 0;
+  }
+  table->rows = 0;
+
+  // Clear row widths.
+  for (int i = 0; i < table->cols; ++i) {
+    table->widths[i] = 0;
+  }
+
+  table->offset = 0;
+}
+
 void uiTableAddRow(uiTable* table, const char** row) {
   assert(table);
 
   table->rows++;
 
-  uiCell* cells =
-      realloc(table->cells, table->rows * table->cols * sizeof(uiCell));
-  assert(cells);
+  uiCell** cells = realloc(table->cells, table->rows * sizeof(uiCell*));
+  ASSERT(cells);
   table->cells = cells;
 
-  uiCell* cell = GetLastRow(table);
-  for (int col = 0; col < table->cols; ++col, ++cell) {
-    cell->child = (uiWidget*)uiMakeLabel(row[col]);
+  uiCell** pLastRow = GetLastRow(table);
+  *pLastRow         = calloc(table->cols, sizeof(uiCell));
+  ASSERT(*pLastRow);
+  uiCell* lastRow = *pLastRow;
+
+  for (int col = 0; col < table->cols; ++col) {
+    lastRow[col].child = (uiWidget*)uiMakeLabel(row[col]);
   }
 }
 
-void uiTableSet(uiTable* table, int row, int col, uiWidgetPtr child) {
+void uiTableSet(uiTable* table, int row, int col, uiPtr child) {
   assert(table);
   assert(child.widget);
 
@@ -618,7 +675,8 @@ static void RenderTable(const uiTable* table, RenderState* state) {
   state->pen.y += g_ui.font->header.glyph_height;
 
   // Render rows.
-  for (int row = 0; (row < table->rows) && PenInSurface(state, 0, 0); ++row) {
+  for (int row = table->offset;
+       (row < table->rows) && PenInSurface(state, 0, 0); ++row) {
     for (int col = 0; (col < table->cols) && PenInSurface(state, 0, 0); ++col) {
       // Crop the column contents to the column width so that one column does
       // not spill into the next.
@@ -688,3 +746,188 @@ void uiRender(const uiFrame* frame, uiSurface* surface) {
   },
       (const uiWidget*)frame);
 }
+
+// -----------------------------------------------------------------------------
+// UI Events.
+
+static void PushWidgetEvent(uiWidgetEvent* event) {
+  assert(event);
+  assert(g_ui.num_widget_events < MaxWidgetEvents);
+
+  g_ui.widget_events[g_ui.num_widget_events++] = *event;
+}
+
+int uiGetEvents(uiWidgetEvent const** ppWidgetEvents) {
+  assert(ppWidgetEvents);
+
+  const int count        = g_ui.num_widget_events;
+  g_ui.num_widget_events = 0;
+
+  *ppWidgetEvents = g_ui.widget_events;
+  return count;
+}
+
+// -----------------------------------------------------------------------------
+// User input.
+
+static bool RectContains(uiRect rect, uiPoint point) {
+  return (rect.x <= point.x) && (point.x <= (rect.x + rect.width)) &&
+         (rect.y <= point.y) && (point.y <= (rect.y + rect.height));
+}
+
+static uiWidget* GetWidgetUnderMouse(uiWidget* parent, uiPoint mouse) {
+  assert(parent);
+
+  // First check the children so that the selection is from "most specific" to
+  // "less specific" from the user's perspective.
+  list_foreach(parent->children, child, {
+    uiWidget* target = GetWidgetUnderMouse(child, mouse);
+    if (target != 0) {
+      return target;
+    }
+  });
+
+  if (RectContains(parent->rect, mouse)) {
+    return parent;
+  }
+
+  return 0;
+}
+
+static void GetTableRowColAtXy(
+    const uiTable* table, uiPoint p, int* out_row, int* out_col) {
+  assert(table);
+  assert(out_row);
+  assert(out_col);
+
+  const uiWidget* widget = (uiWidget*)table;
+
+  int col = -1;
+  int row = -1;
+
+  if (RectContains(widget->rect, p)) {
+    int x = p.x - widget->rect.x;
+    for (col = 0; (col < table->cols) && (x > table->widths[col]); ++col) {
+      x -= table->widths[col];
+    }
+    // 0 is the header and we want to map the first row to 0, so -1.
+    row = table->offset +
+          ((p.y - widget->rect.y) / g_ui.font->header.glyph_height) - 1;
+    // Out-of-bounds check.
+    if ((col >= table->cols) || (row >= table->rows)) {
+      col = row = -1;
+    }
+  }
+
+  *out_col = col;
+  *out_row = row;
+}
+
+static void ClickTable(uiTable* table, const uiMouseClickEvent* event) {
+  assert(table);
+  assert(event);
+
+  int row, col;
+  GetTableRowColAtXy(table, event->mouse_position, &row, &col);
+
+  if ((row != -1) && (col != -1)) {
+    PushWidgetEvent(&(uiWidgetEvent){
+        .type        = uiWidgetEventClick,
+        .widget      = uiMakeTablePtr(table),
+        .table_click = (uiTableClickEvent){.row = row, .col = col}
+    });
+  }
+}
+
+static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) {
+  assert(table);
+  assert(event);
+  table->offset = Max(0, table->offset - event->scroll_offset);
+}
+
+static bool ProcessScrollEvent(
+    uiWidget* widget, const uiMouseScrollEvent* event) {
+  assert(widget);
+  assert(event);
+
+  bool processed = false;
+
+  switch (widget->type) {
+  case uiTypeTable:
+    ScrollTable((uiTable*)widget, event);
+    processed = true;
+    break;
+  default:
+    break;
+  }
+
+  return processed;
+}
+
+static bool ProcessClickEvent(
+    uiWidget* widget, const uiMouseClickEvent* event) {
+  assert(widget);
+  assert(event);
+
+  bool processed = false;
+
+  switch (widget->type) {
+  case uiTypeTable:
+    ClickTable((uiTable*)widget, event);
+    processed = true;
+    break;
+  default:
+    break;
+  }
+
+  return processed;
+}
+
+bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) {
+  assert(frame);
+  assert(event);
+
+  uiWidget* widget = (uiWidget*)frame;
+
+  bool processed = false;
+
+  switch (event->type) {
+  case uiEventMouseButton: {
+    const uiMouseButtonEvent* ev = &event->mouse_button;
+
+    uiMouseButtonState* prev_state = &g_ui.mouse_button_state[ev->button];
+
+    if ((*prev_state == uiMouseDown) && (ev->state == uiMouseUp)) {
+      // Click.
+      uiSendEvent(
+          frame,
+          &(uiInputEvent){
+              .type        = uiEventMouseClick,
+              .mouse_click = (uiMouseClickEvent){
+                                                 .button = ev->button, .mouse_position = ev->mouse_position}
+      });
+    }
+
+    *prev_state = ev->state;
+    break;
+  }
+  case uiEventMouseClick: {
+    const uiMouseClickEvent* ev = &event->mouse_click;
+    uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position);
+    if (target) {
+      processed = ProcessClickEvent(target, ev);
+    }
+    break;
+  }
+  case uiEventMouseScroll: {
+    const uiMouseScrollEvent* ev = &event->mouse_scroll;
+    uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position);
+    if (target) {
+      processed = ProcessScrollEvent(target, ev);
+    }
+    break;
+  }
+  }
+
+  return processed;
+}
-- 
cgit v1.2.3