From a4294e4a94189dffb1fdf99c9a60d87d77272926 Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Sat, 13 Jul 2024 10:52:24 -0700 Subject: Restructure project. --- src/ui.c | 906 +-------------------------------------------------------------- 1 file changed, 2 insertions(+), 904 deletions(-) (limited to 'src/ui.c') diff --git a/src/ui.c b/src/ui.c index e8c8ee2..4d09584 100644 --- a/src/ui.c +++ b/src/ui.c @@ -1,87 +1,11 @@ #include -#include -#include -#include -#include - -#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); - return mem; -} - -#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE)) -#define UI_DEL(ppWidget) \ - { \ - assert(ppWidget); \ - void* widget_ = *ppWidget; \ - if (widget_) { \ - free(widget_); \ - *ppWidget = 0; \ - } \ - } - -DEF_LIST(Widget, uiWidget*) - -/// Base widget type. -typedef struct uiWidget { - uiWidgetType type; - uiRect rect; - Widget_list children; -} uiWidget; - -/// Button. -typedef struct uiButton { - uiWidget widget; - string text; -} uiButton; - -/// Frame. -typedef struct uiFrame { - uiWidget widget; -} uiFrame; - -/// Label. -typedef struct uiLabel { - uiWidget widget; - string text; -} uiLabel; - -/// Table cell. -typedef struct uiCell { - uiWidget* child; -} uiCell; - -/// Table. -typedef struct uiTable { - uiWidget widget; - int rows; - int cols; - 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; - uiMouseButtonState mouse_button_state[uiMouseButtonMax]; - uiWidgetEvent widget_events[MaxWidgetEvents]; - int num_widget_events; -} uiLibrary; +#include "uiLibrary.h" +#include "widget/widget.h" // ----------------------------------------------------------------------------- // Library. -uiLibrary g_ui = {0}; - bool uiInit(void) { // TODO: Embed the font into the library instead. const char* font_path = "../ui/fontbaker/NK57.bin"; @@ -105,829 +29,3 @@ bool uiInit(void) { } void uiShutdown(void) {} - -// ----------------------------------------------------------------------------- -// 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}; -} - -uiButton* uiGetButtonPtr(uiPtr ptr) { - assert(ptr.type == uiTypeButton); - assert(ptr.button); - return ptr.button; -} - -uiFrame* uiGetFramePtr(uiPtr ptr) { - assert(ptr.type == uiTypeFrame); - assert(ptr.frame); - return ptr.frame; -} - -uiLabel* uiGetLabelPtr(uiPtr ptr) { - assert(ptr.type == uiTypeLabel); - assert(ptr.label); - return ptr.label; -} - -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); - - uiWidget* widget = *ppWidget; - if (widget) { - list_foreach_mut(widget->children, child, { DestroyWidget(&child); }); - } - UI_DEL(ppWidget); -} - -void uiWidgetSetParent(uiPtr child_, uiPtr parent_) { - uiWidget* child = child_.widget; - uiWidget* parent = parent_.widget; - - assert(child); - assert(parent); - - list_add(parent->children, child); -} - -// ----------------------------------------------------------------------------- -// Button. - -uiButton* uiMakeButton(const char* text) { - assert(text); - - uiButton* button = UI_NEW(uiButton); - - *button = (uiButton){ - .widget = - (uiWidget){ - .type = uiTypeButton, - .rect = {0}, - }, - .text = string_new(text), - }; - return button; -} - -// ----------------------------------------------------------------------------- -// Label. - -uiLabel* uiMakeLabel(const char* text) { - assert(text); - - uiLabel* label = UI_NEW(uiLabel); - - *label = (uiLabel){ - .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. - -uiFrame* uiMakeFrame(void) { - uiFrame* frame = UI_NEW(uiFrame); - frame->widget.type = uiTypeFrame; - return frame; -} - -void uiDestroyFrame(uiFrame** ppFrame) { DestroyWidget((uiWidget**)ppFrame); } - -uiSize uiGetFrameSize(const uiFrame* frame) { - assert(frame); - return (uiSize){ - .width = frame->widget.rect.width, - .height = frame->widget.rect.height, - }; -} - -// ----------------------------------------------------------------------------- -// Table. - -static const uiCell* GetCell(const uiTable* table, int row, int col) { - assert(table); - return &table->cells[row][col]; -} - -static uiCell* GetCellMut(uiTable* table, int row, int col) { - assert(table); - return (uiCell*)GetCell(table, row, col); -} - -static uiCell** GetLastRow(uiTable* table) { - assert(table); - assert(table->rows > 0); - return &table->cells[table->rows - 1]; -} - -uiTable* uiMakeTable(int rows, int cols, const char** header) { - uiTable* table = UI_NEW(uiTable); - - *table = (uiTable){ - .widget = (uiWidget){.type = uiTypeTable}, - .rows = rows, - .cols = cols, - .widths = (cols > 0) ? calloc(cols, sizeof(int)) : 0, - .header = header ? calloc(cols, sizeof(uiCell)) : 0, - .cells = (rows * cols > 0) ? calloc(rows, sizeof(uiCell*)) : 0, - }; - - if (header) { - for (int col = 0; col < cols; ++col) { - table->header[col].child = (uiWidget*)uiMakeLabel(header[col]); - } - } - - 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 * sizeof(uiCell*)); - ASSERT(cells); - table->cells = cells; - - 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, uiPtr child) { - assert(table); - assert(child.widget); - - GetCellMut(table, row, col)->child = child.widget; -} - -const uiWidget* uiTableGet(const uiTable* table, int row, int col) { - assert(table); - return GetCell(table, row, col)->child; -} - -uiWidget* uiTableGetMut(uiTable* table, int row, int col) { - assert(table); - return GetCellMut(table, row, col)->child; -} - -// ----------------------------------------------------------------------------- -// Layout and resizing. - -static void ResizeTable(uiTable* table, int width, int height) { - assert(table); - - if (table->cols == 0) { - return; - } - - // Surface width: W. - // Columns: N - // - // First, find the minimum width of each column based on their contents. - // - // If the sum of column widths < N, then distribute the extra space first - // among the smallest columns and building up towards the larger. - // - // If the sum of column widths > N, subtract from the largest column first and - // move towards the smaller ones to distribute the space as evenly as - // possible. - - // Find the minimum width for each column. - int* widths = table->widths; - // Header. - for (int col = 0; col < table->cols; ++col) { - const uiCell* cell = &table->header[col]; - const uiLabel* label = (uiLabel*)cell->child; - const int length = (int)string_length(label->text); - - widths[col] = length; - } - // Table contents. - for (int row = 0; row < table->rows; ++row) { - for (int col = 0; col < table->cols; ++col) { - const uiCell* cell = GetCell(table, row, col); - if (cell->child) { - const uiLabel* label = (uiLabel*)cell->child; - const int length = (int)string_length(label->text); - - widths[col] = length > widths[col] ? length : widths[col]; - } - } - } - // Multiply string lengths times glyph width to compute pixel size. - for (int col = 0; col < table->cols; ++col) { - widths[col] *= g_ui.font->header.glyph_width; - } - - // Find the sum of widths. - int used_width = 0; - for (int col = 0; col < table->cols; ++col) { - used_width += widths[col]; - } - - // Pad if available width is larger than sum of widths. - if (used_width < width) { - // Divide evenly among columns. - // const int extra = width - used_width; - // const int pad = extra / table->cols; - // const int mod = extra % table->cols; - // for (int col = 0; col < table->cols; ++col) { - // table->widths[col] += pad + (col < mod ? 1 : 0); - // } - - int extra = width - used_width; - while (extra > 0) { - // Find smallest column. - int smallest = 0; - for (int col = 1; col < table->cols; ++col) { - if (widths[col] < widths[smallest]) { - smallest = col; - } - } - // Pad it and subtract from the budget. - widths[smallest] += 1; - extra--; - } - } - // Shrink if available width is smaller than the sum of widths. - else if (used_width > width) { - int deficit = used_width - width; - while (deficit > 0) { - // Find largest column. - int largest = 0; - for (int col = 1; col < table->cols; ++col) { - if (widths[col] > widths[largest]) { - largest = col; - } - } - // Shrink it and subtract from the deficit. - widths[largest] -= 1; - deficit--; - } - } -} - -static void ResizeWidget(uiWidget* widget, int width, int height) { - assert(widget); - - widget->rect.width = width; - widget->rect.height = height; - - switch (widget->type) { - case uiTypeButton: - break; - case uiTypeFrame: - list_foreach_mut( - widget->children, child, { ResizeWidget(child, width, height); }); - break; - case uiTypeLabel: - break; - case uiTypeTable: - ResizeTable((uiTable*)widget, width, height); - break; - case uiTypeMax: - TRAP(); - break; - } -} - -void uiResizeFrame(uiFrame* frame, int width, int height) { - assert(frame); - ResizeWidget(&frame->widget, width, height); -} - -// ----------------------------------------------------------------------------- -// Rendering. - -static const uiPixel uiBlack = {40, 40, 40, 255}; -static const uiPixel uiWhite = {255, 255, 255, 255}; -static const uiPixel uiPink = {128, 0, 128, 255}; - -/// Render state. -/// -/// Render functions are allowed to manipulate the state internally (e.g., the -/// subsurface), but must leave the state intact before returning, except, of -/// course, for the rendered pixels. -/// -/// We store a subsurface separate from the surface so that we can always check -/// whether a given coordinate is within the bounds of the physical surface. -typedef struct RenderState { - uiSurface surface; /// Surface of pixels on which the UI is rendered. - uiRect subsurface; /// Subregion where the current UI widget is rendered. - uiPoint pen; /// Current pen position relative to subsurface. -} RenderState; - -static void RenderWidget(RenderState* state, const uiWidget* widget); - -void PushSubsurface( - RenderState* state, int width, int height, uiRect* original_subsurface, - uiPoint* original_pen) { - assert(state); - assert(original_subsurface); - assert(original_pen); - - *original_subsurface = state->subsurface; - *original_pen = state->pen; - - state->subsurface.x = state->subsurface.x + state->pen.x; - state->subsurface.width = width; - state->subsurface.height = height; - state->pen.x = 0; -} - -void PopSubsurface( - RenderState* state, const uiRect* original_subsurface, - const uiPoint* original_pen) { - assert(state); - assert(original_subsurface); - assert(original_pen); - - state->subsurface = *original_subsurface; - state->pen = *original_pen; -} - -/// Checks whether pen + (w,h) is within the surface and subsurface. -static bool PenInSurface(const RenderState* state, int w, int h) { - assert(state); - - // Surface. - const bool in_surface = - ((state->subsurface.x + state->pen.x + w) < state->surface.width) && - ((state->subsurface.y + state->pen.y + h) < state->surface.height); - - // Subsurface. - const bool in_subsurface = ((state->pen.x + w) < state->subsurface.width) && - ((state->pen.y + h) < state->subsurface.height); - - return in_surface && in_subsurface; -} - -/// Get the pixel at (x,y). -static uiPixel* SurfaceXy(uiSurface* surface, int x, int y) { - assert(surface); - assert(x >= 0); - assert(y >= 0); - assert(x < surface->width); - assert(y < surface->height); - return surface->pixels + (surface->width * y) + x; -} - -/// Get the pixel at pen + (x,y). -static uiPixel* PixelXy(RenderState* state, int x, int y) { - assert(state); - return SurfaceXy( - &state->surface, state->subsurface.x + state->pen.x + x, - state->subsurface.y + state->pen.y + y); -} - -static void FillRect(const uiRect* rect, uiPixel colour, RenderState* state) { - assert(rect); - assert(state); - assert(rect->width <= state->subsurface.width); - assert(rect->height <= state->subsurface.height); - - for (int y = rect->y; y < rect->y + rect->height; ++y) { - uiPixel* pixel = PixelXy(state, rect->x, y); - for (int x = rect->x; x < rect->x + rect->width; ++x) { - *pixel++ = colour; - } - } -} - -/// Render a glyph. -/// The glyph is clamped to the surface's bounds. -static void RenderGlyph( - const FontAtlas* atlas, unsigned char c, RenderState* state) { - assert(atlas); - assert(state); - assert(atlas->header.glyph_width <= state->subsurface.width); - assert(atlas->header.glyph_height <= state->subsurface.height); - - const int glyph_width = atlas->header.glyph_width; - const int glyph_height = atlas->header.glyph_height; - - const unsigned char* glyph = FontGetGlyph(atlas, c); - - for (int y = 0; (y < atlas->header.glyph_height) && - PenInSurface(state, glyph_width - 1, glyph_height - 1); - ++y) { - for (int x = 0; (x < atlas->header.glyph_width) && - PenInSurface(state, glyph_width - 1, glyph_height - 1); - ++x, ++glyph) { - uiPixel* pixel = PixelXy(state, x, y); - if (*glyph > 0) { - pixel->r = *glyph; - pixel->g = *glyph; - pixel->b = *glyph; - pixel->a = 255; - } - } - } -} - -static void RenderText(const char* text, size_t length, RenderState* state) { - assert(text); - assert(state); - - const FontAtlas* atlas = g_ui.font; - - const int glyph_width = atlas->header.glyph_width; - const int glyph_height = atlas->header.glyph_height; - - // Save the x-pen so that we can restore it after rendering the text. - const int x0 = state->pen.x; - - // Truncate the text rendering if it exceeds the subsurface's width or height. - const char* c = text; - for (size_t i = 0; - (i < length) && PenInSurface(state, glyph_width - 1, glyph_height - 1); - ++i, ++c, state->pen.x += glyph_width) { - RenderGlyph(atlas, *c, state); - } - - state->pen.x = x0; -} - -static void RenderFrame(const uiFrame* frame, RenderState* state) { - assert(frame); - - FillRect(&frame->widget.rect, uiBlack, state); -} - -static void RenderLabel(const uiLabel* label, RenderState* state) { - assert(label); - assert(state); - - RenderText(string_data(label->text), string_length(label->text), state); -} - -static void RenderTable(const uiTable* table, RenderState* state) { - assert(table); - assert(state); - - const int x0 = state->pen.x; - const int y0 = state->pen.y; - - uiRect original_subsurface = {0}; - uiPoint original_pen = {0}; - - // Render header. - if (table->header) { - for (int col = 0; col < table->cols; ++col) { - // Crop the column contents to the column width so that one column does - // not spill into the next. - PushSubsurface( - state, table->widths[col], state->subsurface.height, - &original_subsurface, &original_pen); - - const uiCell* cell = &table->header[col]; - RenderWidget(state, cell->child); - - // Reset the original subsurface and pen for subsequent columns. - PopSubsurface(state, &original_subsurface, &original_pen); - - // Next column. - state->pen.x += table->widths[col]; - } - } - state->pen.x = x0; - state->pen.y += g_ui.font->header.glyph_height; - - // Render rows. - 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. - PushSubsurface( - state, table->widths[col], state->subsurface.height, - &original_subsurface, &original_pen); - - state->subsurface.x = state->subsurface.x + state->pen.x; - state->subsurface.width = table->widths[col]; - state->pen.x = 0; - - const uiCell* cell = GetCell(table, row, col); - RenderWidget(state, cell->child); - - // Reset the original subsurface and pen for subsequent columns. - PopSubsurface(state, &original_subsurface, &original_pen); - - // Next column. - state->pen.x += table->widths[col]; - } - state->pen.x = x0; - state->pen.y += g_ui.font->header.glyph_height; - } - state->pen.y = y0; -} - -static void RenderWidget(RenderState* state, const uiWidget* widget) { - assert(state); - assert(widget); - - // Render this widget. - switch (widget->type) { - case uiTypeButton: - break; - case uiTypeFrame: - RenderFrame((const uiFrame*)widget, state); - break; - case uiTypeLabel: - RenderLabel((const uiLabel*)widget, state); - break; - case uiTypeTable: - RenderTable((const uiTable*)widget, state); - break; - case uiTypeMax: - TRAP(); - break; - } - - // Render children. - list_foreach(widget->children, child, { RenderWidget(state, child); }); -} - -void uiRender(const uiFrame* frame, uiSurface* surface) { - assert(frame); - assert(surface); - - RenderWidget( - &(RenderState){ - .surface = *surface, - .subsurface = - (uiRect){ - .x = 0, - .y = 0, - .width = surface->width, - .height = surface->height}, - .pen = {.x = 0, .y = 0}, - }, - (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