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 +#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