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. --- CMakeLists.txt | 17 +- src/constants.h | 7 + src/event.c | 23 ++ src/event.h | 6 + src/input.c | 178 +++++++++++ src/layout.c | 146 +++++++++ src/render.c | 283 ++++++++++++++++ src/ui.c | 906 +--------------------------------------------------- src/uiLibrary.c | 3 + src/uiLibrary.h | 16 + src/widget/button.c | 19 ++ src/widget/frame.c | 19 ++ src/widget/label.c | 28 ++ src/widget/table.c | 103 ++++++ src/widget/table.h | 11 + src/widget/widget.c | 93 ++++++ src/widget/widget.h | 66 ++++ 17 files changed, 1019 insertions(+), 905 deletions(-) create mode 100644 src/constants.h create mode 100644 src/event.c create mode 100644 src/event.h create mode 100644 src/input.c create mode 100644 src/layout.c create mode 100644 src/render.c create mode 100644 src/uiLibrary.c create mode 100644 src/uiLibrary.h create mode 100644 src/widget/button.c create mode 100644 src/widget/frame.c create mode 100644 src/widget/label.c create mode 100644 src/widget/table.c create mode 100644 src/widget/table.h create mode 100644 src/widget/widget.c create mode 100644 src/widget/widget.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b046871..eeda930 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,11 +10,26 @@ add_subdirectory(font) add_subdirectory(fontbaker) add_library(ui - src/ui.c) + src/event.c + src/input.c + src/layout.c + src/render.c + src/ui.c + src/uiLibrary.c + src/uiLibrary.h + src/widget/button.c + src/widget/frame.c + src/widget/label.c + src/widget/table.c + src/widget/widget.c + src/widget/widget.h) target_include_directories(ui PUBLIC include) +target_include_directories(ui PRIVATE + src) + target_link_libraries(ui PUBLIC cassert cstring diff --git a/src/constants.h b/src/constants.h new file mode 100644 index 0000000..457f461 --- /dev/null +++ b/src/constants.h @@ -0,0 +1,7 @@ +#pragma once + +// Maximum number of events that can be stored in a single input loop. +#define MaxWidgetEvents 8 + +// Width of scroll bars in pixels. +#define ScrollBarWidth 16 diff --git a/src/event.c b/src/event.c new file mode 100644 index 0000000..9b98ee6 --- /dev/null +++ b/src/event.c @@ -0,0 +1,23 @@ +#include "event.h" + +#include "constants.h" +#include "uiLibrary.h" + +#include + +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; +} diff --git a/src/event.h b/src/event.h new file mode 100644 index 0000000..d116fa9 --- /dev/null +++ b/src/event.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +/// Push an event into the widget library. +void PushWidgetEvent(uiWidgetEvent* event); diff --git a/src/input.c b/src/input.c new file mode 100644 index 0000000..c4b1be7 --- /dev/null +++ b/src/input.c @@ -0,0 +1,178 @@ +#include + +#include "event.h" +#include "uiLibrary.h" +#include "widget/widget.h" + +#include + +#define Max(a, b) ((a) > (b) ? (a) : (b)) + +/// Return true if the rectangle contains the point. +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)); +} + +/// Get the bottom-most widget under the given mouse position. +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; +} + +/// Get the table row at the given pixel position. +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; +} + +/// Process a table click event. +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} + }); + } +} + +/// Process a table scroll event. +static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) { + assert(table); + assert(event); + table->offset = Max(0, table->offset - event->scroll_offset); +} + +/// Process a scroll event. +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; +} + +/// Process a click event. +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; +} diff --git a/src/layout.c b/src/layout.c new file mode 100644 index 0000000..9d4b556 --- /dev/null +++ b/src/layout.c @@ -0,0 +1,146 @@ +#include + +#include "uiLibrary.h" +#include "widget/table.h" +#include "widget/widget.h" + +#include + +static void ResizeTable(uiTable* table, int width, int height) { + assert(table); + + if (table->cols == 0) { + return; + } + + // Determine if there is vertical overflow. This determines whether we need to + // render a scroll bar, in which case room must be made for it. + table->flags.vertical_overflow = + (table->rows * g_ui.font->header.glyph_height) > + table->widget.rect.height; + + // 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--; + } + } + + // Now make room for the scroll bar, if necessary. + if (table->flags.vertical_overflow) { + const int offset = ScrollBarWidth / table->cols; + const int remainder = ScrollBarWidth % table->cols; + for (int col = 0; col < table->cols; ++col) { + table->widths[col] -= offset + (col < remainder ? 1 : 0); + assert(table->widths[col] >= 0); + } + } +} + +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); +} diff --git a/src/render.c b/src/render.c new file mode 100644 index 0000000..24490c0 --- /dev/null +++ b/src/render.c @@ -0,0 +1,283 @@ +#include + +#include "uiLibrary.h" +#include "widget/table.h" +#include "widget/widget.h" + +#include +#include + +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); + +/// Push a new subsurface onto which subsequent UI widgets are rendered. +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; +} + +/// Restore the previous subsurface. +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; +} + +/// Check 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); +} + +/// Fill a rectangle with a constant colour. +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; + } + } + } +} + +/// Render text. +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; +} + +/// Render a frame. +static void RenderFrame(const uiFrame* frame, RenderState* state) { + assert(frame); + + FillRect(&frame->widget.rect, uiBlack, state); +} + +/// Render a label. +static void RenderLabel(const uiLabel* label, RenderState* state) { + assert(label); + assert(state); + + RenderText(string_data(label->text), string_length(label->text), state); +} + +/// Render a table. +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; +} + +/// Render a widget. +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); +} 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; -} diff --git a/src/uiLibrary.c b/src/uiLibrary.c new file mode 100644 index 0000000..fbacdcc --- /dev/null +++ b/src/uiLibrary.c @@ -0,0 +1,3 @@ +#include "uiLibrary.h" + +uiLibrary g_ui = {0}; diff --git a/src/uiLibrary.h b/src/uiLibrary.h new file mode 100644 index 0000000..98719d7 --- /dev/null +++ b/src/uiLibrary.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include "constants.h" + +#include + +typedef struct uiLibrary { + FontAtlas* font; + uiMouseButtonState mouse_button_state[uiMouseButtonMax]; + uiWidgetEvent widget_events[MaxWidgetEvents]; + int num_widget_events; +} uiLibrary; + +extern uiLibrary g_ui; diff --git a/src/widget/button.c b/src/widget/button.c new file mode 100644 index 0000000..f2313fd --- /dev/null +++ b/src/widget/button.c @@ -0,0 +1,19 @@ +#include + +#include "widget.h" + +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; +} diff --git a/src/widget/frame.c b/src/widget/frame.c new file mode 100644 index 0000000..e1078be --- /dev/null +++ b/src/widget/frame.c @@ -0,0 +1,19 @@ +#include + +#include "widget.h" + +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, + }; +} diff --git a/src/widget/label.c b/src/widget/label.c new file mode 100644 index 0000000..30ca0ec --- /dev/null +++ b/src/widget/label.c @@ -0,0 +1,28 @@ +#include + +#include "uiLibrary.h" +#include "widget.h" + +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); +} diff --git a/src/widget/table.c b/src/widget/table.c new file mode 100644 index 0000000..7a0ea03 --- /dev/null +++ b/src/widget/table.c @@ -0,0 +1,103 @@ +#include "table.h" + +#include "widget.h" + +const uiCell* GetCell(const uiTable* table, int row, int col) { + assert(table); + return &table->cells[row][col]; +} + +uiCell* GetCellMut(uiTable* table, int row, int col) { + assert(table); + return (uiCell*)GetCell(table, row, col); +} + +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, + .flags = {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; + + table->flags.vertical_overflow = 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; +} diff --git a/src/widget/table.h b/src/widget/table.h new file mode 100644 index 0000000..9f466de --- /dev/null +++ b/src/widget/table.h @@ -0,0 +1,11 @@ +#pragma once + +#include + +#include "widget.h" + +const uiCell* GetCell(const uiTable* table, int row, int col); + +uiCell* GetCellMut(uiTable* table, int row, int col); + +uiCell** GetLastRow(uiTable* table); diff --git a/src/widget/widget.c b/src/widget/widget.c new file mode 100644 index 0000000..ef79ac4 --- /dev/null +++ b/src/widget/widget.c @@ -0,0 +1,93 @@ +#include "widget.h" + +#include + +// ----------------------------------------------------------------------------- +// Widget. + +#define UI_DEL(ppWidget) \ + { \ + assert(ppWidget); \ + void* widget_ = *ppWidget; \ + if (widget_) { \ + free(widget_); \ + *ppWidget = 0; \ + } \ + } + +uiWidgetType uiWidgetGetType(const uiWidget* widget) { + assert(widget); + return widget->type; +} + +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); +} + +// ----------------------------------------------------------------------------- +// 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; +} diff --git a/src/widget/widget.h b/src/widget/widget.h new file mode 100644 index 0000000..a2c96bc --- /dev/null +++ b/src/widget/widget.h @@ -0,0 +1,66 @@ +#pragma once + +#include + +#include +#include + +#include + +DEF_LIST(Widget, uiWidget*) + +#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE)) + +static inline void* uiAlloc(size_t count, size_t size) { + void* mem = calloc(count, size); + ASSERT(mem); + return mem; +} + +// ----------------------------------------------------------------------------- +// Widgets. + +/// 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. + struct { + bool vertical_overflow : 1; // True if contents overflow vertically. + } flags; +} uiTable; + +void DestroyWidget(uiWidget** ppWidget); -- cgit v1.2.3