diff options
author | 3gg <3gg@shellblade.net> | 2024-06-22 13:23:42 -0700 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2024-06-22 13:23:42 -0700 |
commit | 2f2d42e28a14cdc856f8cf0c45cd572646be6750 (patch) | |
tree | 441d759cc8a34898aef2d33686b925c0ff27bd23 /src/ui.c | |
parent | af641426fad35cd857c1f14bda523db3d85a70cd (diff) |
Table user input.
Diffstat (limited to 'src/ui.c')
-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 | } | ||