diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/ui.c | 329 |
1 files changed, 286 insertions, 43 deletions
| @@ -7,6 +7,10 @@ | |||
| 7 | 7 | ||
| 8 | #include <stdlib.h> | 8 | #include <stdlib.h> |
| 9 | 9 | ||
| 10 | #define Max(a, b) ((a) > (b) ? (a) : (b)) | ||
| 11 | |||
| 12 | #define MaxWidgetEvents 8 | ||
| 13 | |||
| 10 | static void* uiAlloc(size_t count, size_t size) { | 14 | static void* uiAlloc(size_t count, size_t size) { |
| 11 | void* mem = calloc(count, size); | 15 | void* mem = calloc(count, size); |
| 12 | ASSERT(mem); | 16 | ASSERT(mem); |
| @@ -60,13 +64,17 @@ typedef struct uiTable { | |||
| 60 | uiWidget widget; | 64 | uiWidget widget; |
| 61 | int rows; | 65 | int rows; |
| 62 | int cols; | 66 | int cols; |
| 63 | int* widths; /// Width, in pixels, for each each column. | 67 | int* widths; // Width, in pixels, for each column. |
| 64 | uiCell* header; /// If non-null, row of 'cols' header cells. | 68 | uiCell* header; // If non-null, row of 'cols' header cells. |
| 65 | uiCell* cells; /// Array of 'rows * cols' cells. | 69 | uiCell** cells; // Array of 'rows' rows, each of 'cols' cells. |
| 70 | int offset; // Offset into the rows of the table. Units: rows. | ||
| 66 | } uiTable; | 71 | } uiTable; |
| 67 | 72 | ||
| 68 | typedef struct uiLibrary { | 73 | typedef struct uiLibrary { |
| 69 | FontAtlas* font; | 74 | FontAtlas* font; |
| 75 | uiMouseButtonState mouse_button_state[uiMouseButtonMax]; | ||
| 76 | uiWidgetEvent widget_events[MaxWidgetEvents]; | ||
| 77 | int num_widget_events; | ||
| 70 | } uiLibrary; | 78 | } uiLibrary; |
| 71 | 79 | ||
| 72 | // ----------------------------------------------------------------------------- | 80 | // ----------------------------------------------------------------------------- |
| @@ -99,32 +107,65 @@ bool uiInit(void) { | |||
| 99 | void uiShutdown(void) {} | 107 | void uiShutdown(void) {} |
| 100 | 108 | ||
| 101 | // ----------------------------------------------------------------------------- | 109 | // ----------------------------------------------------------------------------- |
| 102 | // Widget. | 110 | // Widget pointers. |
| 111 | |||
| 112 | uiPtr uiMakeButtonPtr(uiButton* button) { | ||
| 113 | assert(button); | ||
| 114 | return (uiPtr){.type = uiTypeButton, .button = button}; | ||
| 115 | } | ||
| 116 | |||
| 117 | uiPtr uiMakeFramePtr(uiFrame* frame) { | ||
| 118 | assert(frame); | ||
| 119 | return (uiPtr){.type = uiTypeFrame, .frame = frame}; | ||
| 120 | } | ||
| 121 | |||
| 122 | uiPtr uiMakeLabelPtr(uiLabel* label) { | ||
| 123 | assert(label); | ||
| 124 | return (uiPtr){.type = uiTypeLabel, .label = label}; | ||
| 125 | } | ||
| 126 | |||
| 127 | uiPtr uiMakeTablePtr(uiTable* table) { | ||
| 128 | assert(table); | ||
| 129 | return (uiPtr){.type = uiTypeTable, .table = table}; | ||
| 130 | } | ||
| 131 | |||
| 132 | static uiPtr uiMakeWidgetPtr(uiWidget* widget) { | ||
| 133 | assert(widget); | ||
| 134 | return (uiPtr){.type = widget->type, .widget = widget}; | ||
| 135 | } | ||
| 103 | 136 | ||
| 104 | static uiButton* uiGetButtonPtr(uiWidgetPtr ptr) { | 137 | uiButton* uiGetButtonPtr(uiPtr ptr) { |
| 105 | assert(ptr.type == uiTypeButton); | 138 | assert(ptr.type == uiTypeButton); |
| 106 | assert(ptr.button); | 139 | assert(ptr.button); |
| 107 | return ptr.button; | 140 | return ptr.button; |
| 108 | } | 141 | } |
| 109 | 142 | ||
| 110 | static uiFrame* uiGetFramePtr(uiWidgetPtr ptr) { | 143 | uiFrame* uiGetFramePtr(uiPtr ptr) { |
| 111 | assert(ptr.type == uiTypeFrame); | 144 | assert(ptr.type == uiTypeFrame); |
| 112 | assert(ptr.frame); | 145 | assert(ptr.frame); |
| 113 | return ptr.frame; | 146 | return ptr.frame; |
| 114 | } | 147 | } |
| 115 | 148 | ||
| 116 | static uiLabel* uiGetLabelPtr(uiWidgetPtr ptr) { | 149 | uiLabel* uiGetLabelPtr(uiPtr ptr) { |
| 117 | assert(ptr.type == uiTypeLabel); | 150 | assert(ptr.type == uiTypeLabel); |
| 118 | assert(ptr.label); | 151 | assert(ptr.label); |
| 119 | return ptr.label; | 152 | return ptr.label; |
| 120 | } | 153 | } |
| 121 | 154 | ||
| 122 | static uiTable* uiGetTablePtr(uiWidgetPtr ptr) { | 155 | uiTable* uiGetTablePtr(uiPtr ptr) { |
| 123 | assert(ptr.type == uiTypeTable); | 156 | assert(ptr.type == uiTypeTable); |
| 124 | assert(ptr.table); | 157 | assert(ptr.table); |
| 125 | return ptr.table; | 158 | return ptr.table; |
| 126 | } | 159 | } |
| 127 | 160 | ||
| 161 | // ----------------------------------------------------------------------------- | ||
| 162 | // Widget. | ||
| 163 | |||
| 164 | uiWidgetType uiWidgetGetType(const uiWidget* widget) { | ||
| 165 | assert(widget); | ||
| 166 | return widget->type; | ||
| 167 | } | ||
| 168 | |||
| 128 | static void DestroyWidget(uiWidget** ppWidget) { | 169 | static void DestroyWidget(uiWidget** ppWidget) { |
| 129 | assert(ppWidget); | 170 | assert(ppWidget); |
| 130 | 171 | ||
| @@ -135,27 +176,7 @@ static void DestroyWidget(uiWidget** ppWidget) { | |||
| 135 | UI_DEL(ppWidget); | 176 | UI_DEL(ppWidget); |
| 136 | } | 177 | } |
| 137 | 178 | ||
| 138 | uiWidgetPtr uiMakeButtonPtr(uiButton* button) { | 179 | void uiWidgetSetParent(uiPtr child_, uiPtr parent_) { |
| 139 | assert(button); | ||
| 140 | return (uiWidgetPtr){.type = uiTypeButton, .button = button}; | ||
| 141 | } | ||
| 142 | |||
| 143 | uiWidgetPtr uiMakeFramePtr(uiFrame* frame) { | ||
| 144 | assert(frame); | ||
| 145 | return (uiWidgetPtr){.type = uiTypeFrame, .frame = frame}; | ||
| 146 | } | ||
| 147 | |||
| 148 | uiWidgetPtr uiMakeLabelPtr(uiLabel* label) { | ||
| 149 | assert(label); | ||
| 150 | return (uiWidgetPtr){.type = uiTypeLabel, .label = label}; | ||
| 151 | } | ||
| 152 | |||
| 153 | uiWidgetPtr uiMakeTablePtr(uiTable* table) { | ||
| 154 | assert(table); | ||
| 155 | return (uiWidgetPtr){.type = uiTypeTable, .table = table}; | ||
| 156 | } | ||
| 157 | |||
| 158 | void uiWidgetSetParent(uiWidgetPtr child_, uiWidgetPtr parent_) { | ||
| 159 | uiWidget* child = child_.widget; | 180 | uiWidget* child = child_.widget; |
| 160 | uiWidget* parent = parent_.widget; | 181 | uiWidget* parent = parent_.widget; |
| 161 | 182 | ||
| @@ -196,12 +217,21 @@ uiLabel* uiMakeLabel(const char* text) { | |||
| 196 | .widget = | 217 | .widget = |
| 197 | (uiWidget){ | 218 | (uiWidget){ |
| 198 | .type = uiTypeLabel, | 219 | .type = uiTypeLabel, |
| 199 | }, | 220 | .rect = |
| 221 | (uiRect){ | ||
| 222 | .width = | ||
| 223 | (int)strlen(text) * g_ui.font->header.glyph_width, | ||
| 224 | .height = g_ui.font->header.glyph_height}}, | ||
| 200 | .text = string_new(text), | 225 | .text = string_new(text), |
| 201 | }; | 226 | }; |
| 202 | return label; | 227 | return label; |
| 203 | } | 228 | } |
| 204 | 229 | ||
| 230 | const char* uiLabelGetText(const uiLabel* label) { | ||
| 231 | assert(label); | ||
| 232 | return string_data(label->text); | ||
| 233 | } | ||
| 234 | |||
| 205 | // ----------------------------------------------------------------------------- | 235 | // ----------------------------------------------------------------------------- |
| 206 | // Frame. | 236 | // Frame. |
| 207 | 237 | ||
| @@ -226,7 +256,7 @@ uiSize uiGetFrameSize(const uiFrame* frame) { | |||
| 226 | 256 | ||
| 227 | static const uiCell* GetCell(const uiTable* table, int row, int col) { | 257 | static const uiCell* GetCell(const uiTable* table, int row, int col) { |
| 228 | assert(table); | 258 | assert(table); |
| 229 | return table->cells + (row * table->cols) + col; | 259 | return &table->cells[row][col]; |
| 230 | } | 260 | } |
| 231 | 261 | ||
| 232 | static uiCell* GetCellMut(uiTable* table, int row, int col) { | 262 | static uiCell* GetCellMut(uiTable* table, int row, int col) { |
| @@ -234,10 +264,10 @@ static uiCell* GetCellMut(uiTable* table, int row, int col) { | |||
| 234 | return (uiCell*)GetCell(table, row, col); | 264 | return (uiCell*)GetCell(table, row, col); |
| 235 | } | 265 | } |
| 236 | 266 | ||
| 237 | static uiCell* GetLastRow(uiTable* table) { | 267 | static uiCell** GetLastRow(uiTable* table) { |
| 238 | assert(table); | 268 | assert(table); |
| 239 | assert(table->rows > 0); | 269 | assert(table->rows > 0); |
| 240 | return &table->cells[table->cols * (table->rows - 1)]; | 270 | return &table->cells[table->rows - 1]; |
| 241 | } | 271 | } |
| 242 | 272 | ||
| 243 | uiTable* uiMakeTable(int rows, int cols, const char** header) { | 273 | uiTable* uiMakeTable(int rows, int cols, const char** header) { |
| @@ -249,7 +279,7 @@ uiTable* uiMakeTable(int rows, int cols, const char** header) { | |||
| 249 | .cols = cols, | 279 | .cols = cols, |
| 250 | .widths = (cols > 0) ? calloc(cols, sizeof(int)) : 0, | 280 | .widths = (cols > 0) ? calloc(cols, sizeof(int)) : 0, |
| 251 | .header = header ? calloc(cols, sizeof(uiCell)) : 0, | 281 | .header = header ? calloc(cols, sizeof(uiCell)) : 0, |
| 252 | .cells = (rows * cols > 0) ? calloc(rows * cols, sizeof(uiCell)) : 0, | 282 | .cells = (rows * cols > 0) ? calloc(rows, sizeof(uiCell*)) : 0, |
| 253 | }; | 283 | }; |
| 254 | 284 | ||
| 255 | if (header) { | 285 | if (header) { |
| @@ -261,23 +291,50 @@ uiTable* uiMakeTable(int rows, int cols, const char** header) { | |||
| 261 | return table; | 291 | return table; |
| 262 | } | 292 | } |
| 263 | 293 | ||
| 294 | void uiTableClear(uiTable* table) { | ||
| 295 | assert(table); | ||
| 296 | |||
| 297 | // Free row data. | ||
| 298 | if (table->cells) { | ||
| 299 | for (int row = 0; row < table->rows; ++row) { | ||
| 300 | for (int col = 0; col < table->cols; ++col) { | ||
| 301 | DestroyWidget(&table->cells[row][col].child); | ||
| 302 | } | ||
| 303 | free(table->cells[row]); | ||
| 304 | } | ||
| 305 | free(table->cells); | ||
| 306 | table->cells = 0; | ||
| 307 | } | ||
| 308 | table->rows = 0; | ||
| 309 | |||
| 310 | // Clear row widths. | ||
| 311 | for (int i = 0; i < table->cols; ++i) { | ||
| 312 | table->widths[i] = 0; | ||
| 313 | } | ||
| 314 | |||
| 315 | table->offset = 0; | ||
| 316 | } | ||
| 317 | |||
| 264 | void uiTableAddRow(uiTable* table, const char** row) { | 318 | void uiTableAddRow(uiTable* table, const char** row) { |
| 265 | assert(table); | 319 | assert(table); |
| 266 | 320 | ||
| 267 | table->rows++; | 321 | table->rows++; |
| 268 | 322 | ||
| 269 | uiCell* cells = | 323 | uiCell** cells = realloc(table->cells, table->rows * sizeof(uiCell*)); |
| 270 | realloc(table->cells, table->rows * table->cols * sizeof(uiCell)); | 324 | ASSERT(cells); |
| 271 | assert(cells); | ||
| 272 | table->cells = cells; | 325 | table->cells = cells; |
| 273 | 326 | ||
| 274 | uiCell* cell = GetLastRow(table); | 327 | uiCell** pLastRow = GetLastRow(table); |
| 275 | for (int col = 0; col < table->cols; ++col, ++cell) { | 328 | *pLastRow = calloc(table->cols, sizeof(uiCell)); |
| 276 | cell->child = (uiWidget*)uiMakeLabel(row[col]); | 329 | ASSERT(*pLastRow); |
| 330 | uiCell* lastRow = *pLastRow; | ||
| 331 | |||
| 332 | for (int col = 0; col < table->cols; ++col) { | ||
| 333 | lastRow[col].child = (uiWidget*)uiMakeLabel(row[col]); | ||
| 277 | } | 334 | } |
| 278 | } | 335 | } |
| 279 | 336 | ||
| 280 | void uiTableSet(uiTable* table, int row, int col, uiWidgetPtr child) { | 337 | void uiTableSet(uiTable* table, int row, int col, uiPtr child) { |
| 281 | assert(table); | 338 | assert(table); |
| 282 | assert(child.widget); | 339 | assert(child.widget); |
| 283 | 340 | ||
| @@ -618,7 +675,8 @@ static void RenderTable(const uiTable* table, RenderState* state) { | |||
| 618 | state->pen.y += g_ui.font->header.glyph_height; | 675 | state->pen.y += g_ui.font->header.glyph_height; |
| 619 | 676 | ||
| 620 | // Render rows. | 677 | // Render rows. |
| 621 | for (int row = 0; (row < table->rows) && PenInSurface(state, 0, 0); ++row) { | 678 | for (int row = table->offset; |
| 679 | (row < table->rows) && PenInSurface(state, 0, 0); ++row) { | ||
| 622 | for (int col = 0; (col < table->cols) && PenInSurface(state, 0, 0); ++col) { | 680 | for (int col = 0; (col < table->cols) && PenInSurface(state, 0, 0); ++col) { |
| 623 | // Crop the column contents to the column width so that one column does | 681 | // Crop the column contents to the column width so that one column does |
| 624 | // not spill into the next. | 682 | // not spill into the next. |
| @@ -688,3 +746,188 @@ void uiRender(const uiFrame* frame, uiSurface* surface) { | |||
| 688 | }, | 746 | }, |
| 689 | (const uiWidget*)frame); | 747 | (const uiWidget*)frame); |
| 690 | } | 748 | } |
| 749 | |||
| 750 | // ----------------------------------------------------------------------------- | ||
| 751 | // UI Events. | ||
| 752 | |||
| 753 | static void PushWidgetEvent(uiWidgetEvent* event) { | ||
| 754 | assert(event); | ||
| 755 | assert(g_ui.num_widget_events < MaxWidgetEvents); | ||
| 756 | |||
| 757 | g_ui.widget_events[g_ui.num_widget_events++] = *event; | ||
| 758 | } | ||
| 759 | |||
| 760 | int uiGetEvents(uiWidgetEvent const** ppWidgetEvents) { | ||
| 761 | assert(ppWidgetEvents); | ||
| 762 | |||
| 763 | const int count = g_ui.num_widget_events; | ||
| 764 | g_ui.num_widget_events = 0; | ||
| 765 | |||
| 766 | *ppWidgetEvents = g_ui.widget_events; | ||
| 767 | return count; | ||
| 768 | } | ||
| 769 | |||
| 770 | // ----------------------------------------------------------------------------- | ||
| 771 | // User input. | ||
| 772 | |||
| 773 | static bool RectContains(uiRect rect, uiPoint point) { | ||
| 774 | return (rect.x <= point.x) && (point.x <= (rect.x + rect.width)) && | ||
| 775 | (rect.y <= point.y) && (point.y <= (rect.y + rect.height)); | ||
| 776 | } | ||
| 777 | |||
| 778 | static uiWidget* GetWidgetUnderMouse(uiWidget* parent, uiPoint mouse) { | ||
| 779 | assert(parent); | ||
| 780 | |||
| 781 | // First check the children so that the selection is from "most specific" to | ||
| 782 | // "less specific" from the user's perspective. | ||
| 783 | list_foreach(parent->children, child, { | ||
| 784 | uiWidget* target = GetWidgetUnderMouse(child, mouse); | ||
| 785 | if (target != 0) { | ||
| 786 | return target; | ||
| 787 | } | ||
| 788 | }); | ||
| 789 | |||
| 790 | if (RectContains(parent->rect, mouse)) { | ||
| 791 | return parent; | ||
| 792 | } | ||
| 793 | |||
| 794 | return 0; | ||
| 795 | } | ||
| 796 | |||
| 797 | static void GetTableRowColAtXy( | ||
| 798 | const uiTable* table, uiPoint p, int* out_row, int* out_col) { | ||
| 799 | assert(table); | ||
| 800 | assert(out_row); | ||
| 801 | assert(out_col); | ||
| 802 | |||
| 803 | const uiWidget* widget = (uiWidget*)table; | ||
| 804 | |||
| 805 | int col = -1; | ||
| 806 | int row = -1; | ||
| 807 | |||
| 808 | if (RectContains(widget->rect, p)) { | ||
| 809 | int x = p.x - widget->rect.x; | ||
| 810 | for (col = 0; (col < table->cols) && (x > table->widths[col]); ++col) { | ||
| 811 | x -= table->widths[col]; | ||
| 812 | } | ||
| 813 | // 0 is the header and we want to map the first row to 0, so -1. | ||
| 814 | row = table->offset + | ||
| 815 | ((p.y - widget->rect.y) / g_ui.font->header.glyph_height) - 1; | ||
| 816 | // Out-of-bounds check. | ||
| 817 | if ((col >= table->cols) || (row >= table->rows)) { | ||
| 818 | col = row = -1; | ||
| 819 | } | ||
| 820 | } | ||
| 821 | |||
| 822 | *out_col = col; | ||
| 823 | *out_row = row; | ||
| 824 | } | ||
| 825 | |||
| 826 | static void ClickTable(uiTable* table, const uiMouseClickEvent* event) { | ||
| 827 | assert(table); | ||
| 828 | assert(event); | ||
| 829 | |||
| 830 | int row, col; | ||
| 831 | GetTableRowColAtXy(table, event->mouse_position, &row, &col); | ||
| 832 | |||
| 833 | if ((row != -1) && (col != -1)) { | ||
| 834 | PushWidgetEvent(&(uiWidgetEvent){ | ||
| 835 | .type = uiWidgetEventClick, | ||
| 836 | .widget = uiMakeTablePtr(table), | ||
| 837 | .table_click = (uiTableClickEvent){.row = row, .col = col} | ||
| 838 | }); | ||
| 839 | } | ||
| 840 | } | ||
| 841 | |||
| 842 | static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) { | ||
| 843 | assert(table); | ||
| 844 | assert(event); | ||
| 845 | table->offset = Max(0, table->offset - event->scroll_offset); | ||
| 846 | } | ||
| 847 | |||
| 848 | static bool ProcessScrollEvent( | ||
| 849 | uiWidget* widget, const uiMouseScrollEvent* event) { | ||
| 850 | assert(widget); | ||
| 851 | assert(event); | ||
| 852 | |||
| 853 | bool processed = false; | ||
| 854 | |||
| 855 | switch (widget->type) { | ||
| 856 | case uiTypeTable: | ||
| 857 | ScrollTable((uiTable*)widget, event); | ||
| 858 | processed = true; | ||
| 859 | break; | ||
| 860 | default: | ||
| 861 | break; | ||
| 862 | } | ||
| 863 | |||
| 864 | return processed; | ||
| 865 | } | ||
| 866 | |||
| 867 | static bool ProcessClickEvent( | ||
| 868 | uiWidget* widget, const uiMouseClickEvent* event) { | ||
| 869 | assert(widget); | ||
| 870 | assert(event); | ||
| 871 | |||
| 872 | bool processed = false; | ||
| 873 | |||
| 874 | switch (widget->type) { | ||
| 875 | case uiTypeTable: | ||
| 876 | ClickTable((uiTable*)widget, event); | ||
| 877 | processed = true; | ||
| 878 | break; | ||
| 879 | default: | ||
| 880 | break; | ||
| 881 | } | ||
| 882 | |||
| 883 | return processed; | ||
| 884 | } | ||
| 885 | |||
| 886 | bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) { | ||
| 887 | assert(frame); | ||
| 888 | assert(event); | ||
| 889 | |||
| 890 | uiWidget* widget = (uiWidget*)frame; | ||
| 891 | |||
| 892 | bool processed = false; | ||
| 893 | |||
| 894 | switch (event->type) { | ||
| 895 | case uiEventMouseButton: { | ||
| 896 | const uiMouseButtonEvent* ev = &event->mouse_button; | ||
| 897 | |||
| 898 | uiMouseButtonState* prev_state = &g_ui.mouse_button_state[ev->button]; | ||
| 899 | |||
| 900 | if ((*prev_state == uiMouseDown) && (ev->state == uiMouseUp)) { | ||
| 901 | // Click. | ||
| 902 | uiSendEvent( | ||
| 903 | frame, | ||
| 904 | &(uiInputEvent){ | ||
| 905 | .type = uiEventMouseClick, | ||
| 906 | .mouse_click = (uiMouseClickEvent){ | ||
| 907 | .button = ev->button, .mouse_position = ev->mouse_position} | ||
| 908 | }); | ||
| 909 | } | ||
| 910 | |||
| 911 | *prev_state = ev->state; | ||
| 912 | break; | ||
| 913 | } | ||
| 914 | case uiEventMouseClick: { | ||
| 915 | const uiMouseClickEvent* ev = &event->mouse_click; | ||
| 916 | uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position); | ||
| 917 | if (target) { | ||
| 918 | processed = ProcessClickEvent(target, ev); | ||
| 919 | } | ||
| 920 | break; | ||
| 921 | } | ||
| 922 | case uiEventMouseScroll: { | ||
| 923 | const uiMouseScrollEvent* ev = &event->mouse_scroll; | ||
| 924 | uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position); | ||
| 925 | if (target) { | ||
| 926 | processed = ProcessScrollEvent(target, ev); | ||
| 927 | } | ||
| 928 | break; | ||
| 929 | } | ||
| 930 | } | ||
| 931 | |||
| 932 | return processed; | ||
| 933 | } | ||
