summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2024-07-13 10:52:24 -0700
committer3gg <3gg@shellblade.net>2024-07-13 10:52:24 -0700
commita4294e4a94189dffb1fdf99c9a60d87d77272926 (patch)
tree2e92f7c95116861bc39f4dae1d0ab5d388550000
parentcf9579d7546c04dbc708bd8719e3f935a28088bd (diff)
Restructure project.
-rw-r--r--CMakeLists.txt17
-rw-r--r--src/constants.h7
-rw-r--r--src/event.c23
-rw-r--r--src/event.h6
-rw-r--r--src/input.c178
-rw-r--r--src/layout.c146
-rw-r--r--src/render.c283
-rw-r--r--src/ui.c906
-rw-r--r--src/uiLibrary.c3
-rw-r--r--src/uiLibrary.h16
-rw-r--r--src/widget/button.c19
-rw-r--r--src/widget/frame.c19
-rw-r--r--src/widget/label.c28
-rw-r--r--src/widget/table.c103
-rw-r--r--src/widget/table.h11
-rw-r--r--src/widget/widget.c93
-rw-r--r--src/widget/widget.h66
17 files changed, 1019 insertions, 905 deletions
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)
10add_subdirectory(fontbaker) 10add_subdirectory(fontbaker)
11 11
12add_library(ui 12add_library(ui
13 src/ui.c) 13 src/event.c
14 src/input.c
15 src/layout.c
16 src/render.c
17 src/ui.c
18 src/uiLibrary.c
19 src/uiLibrary.h
20 src/widget/button.c
21 src/widget/frame.c
22 src/widget/label.c
23 src/widget/table.c
24 src/widget/widget.c
25 src/widget/widget.h)
14 26
15target_include_directories(ui PUBLIC 27target_include_directories(ui PUBLIC
16 include) 28 include)
17 29
30target_include_directories(ui PRIVATE
31 src)
32
18target_link_libraries(ui PUBLIC 33target_link_libraries(ui PUBLIC
19 cassert 34 cassert
20 cstring 35 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 @@
1#pragma once
2
3// Maximum number of events that can be stored in a single input loop.
4#define MaxWidgetEvents 8
5
6// Width of scroll bars in pixels.
7#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 @@
1#include "event.h"
2
3#include "constants.h"
4#include "uiLibrary.h"
5
6#include <cassert.h>
7
8void PushWidgetEvent(uiWidgetEvent* event) {
9 assert(event);
10 assert(g_ui.num_widget_events < MaxWidgetEvents);
11
12 g_ui.widget_events[g_ui.num_widget_events++] = *event;
13}
14
15int uiGetEvents(uiWidgetEvent const** ppWidgetEvents) {
16 assert(ppWidgetEvents);
17
18 const int count = g_ui.num_widget_events;
19 g_ui.num_widget_events = 0;
20
21 *ppWidgetEvents = g_ui.widget_events;
22 return count;
23}
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 @@
1#pragma once
2
3#include <ui.h>
4
5/// Push an event into the widget library.
6void 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 @@
1#include <ui.h>
2
3#include "event.h"
4#include "uiLibrary.h"
5#include "widget/widget.h"
6
7#include <cassert.h>
8
9#define Max(a, b) ((a) > (b) ? (a) : (b))
10
11/// Return true if the rectangle contains the point.
12static bool RectContains(uiRect rect, uiPoint point) {
13 return (rect.x <= point.x) && (point.x <= (rect.x + rect.width)) &&
14 (rect.y <= point.y) && (point.y <= (rect.y + rect.height));
15}
16
17/// Get the bottom-most widget under the given mouse position.
18static uiWidget* GetWidgetUnderMouse(uiWidget* parent, uiPoint mouse) {
19 assert(parent);
20
21 // First check the children so that the selection is from "most specific" to
22 // "less specific" from the user's perspective.
23 list_foreach(parent->children, child, {
24 uiWidget* target = GetWidgetUnderMouse(child, mouse);
25 if (target != 0) {
26 return target;
27 }
28 });
29
30 if (RectContains(parent->rect, mouse)) {
31 return parent;
32 }
33
34 return 0;
35}
36
37/// Get the table row at the given pixel position.
38static void GetTableRowColAtXy(
39 const uiTable* table, uiPoint p, int* out_row, int* out_col) {
40 assert(table);
41 assert(out_row);
42 assert(out_col);
43
44 const uiWidget* widget = (uiWidget*)table;
45
46 int col = -1;
47 int row = -1;
48
49 if (RectContains(widget->rect, p)) {
50 int x = p.x - widget->rect.x;
51 for (col = 0; (col < table->cols) && (x > table->widths[col]); ++col) {
52 x -= table->widths[col];
53 }
54 // 0 is the header and we want to map the first row to 0, so -1.
55 row = table->offset +
56 ((p.y - widget->rect.y) / g_ui.font->header.glyph_height) - 1;
57 // Out-of-bounds check.
58 if ((col >= table->cols) || (row >= table->rows)) {
59 col = row = -1;
60 }
61 }
62
63 *out_col = col;
64 *out_row = row;
65}
66
67/// Process a table click event.
68static void ClickTable(uiTable* table, const uiMouseClickEvent* event) {
69 assert(table);
70 assert(event);
71
72 int row, col;
73 GetTableRowColAtXy(table, event->mouse_position, &row, &col);
74
75 if ((row != -1) && (col != -1)) {
76 PushWidgetEvent(&(uiWidgetEvent){
77 .type = uiWidgetEventClick,
78 .widget = uiMakeTablePtr(table),
79 .table_click = (uiTableClickEvent){.row = row, .col = col}
80 });
81 }
82}
83
84/// Process a table scroll event.
85static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) {
86 assert(table);
87 assert(event);
88 table->offset = Max(0, table->offset - event->scroll_offset);
89}
90
91/// Process a scroll event.
92static bool ProcessScrollEvent(
93 uiWidget* widget, const uiMouseScrollEvent* event) {
94 assert(widget);
95 assert(event);
96
97 bool processed = false;
98
99 switch (widget->type) {
100 case uiTypeTable:
101 ScrollTable((uiTable*)widget, event);
102 processed = true;
103 break;
104 default:
105 break;
106 }
107
108 return processed;
109}
110
111/// Process a click event.
112static bool ProcessClickEvent(
113 uiWidget* widget, const uiMouseClickEvent* event) {
114 assert(widget);
115 assert(event);
116
117 bool processed = false;
118
119 switch (widget->type) {
120 case uiTypeTable:
121 ClickTable((uiTable*)widget, event);
122 processed = true;
123 break;
124 default:
125 break;
126 }
127
128 return processed;
129}
130
131bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) {
132 assert(frame);
133 assert(event);
134
135 uiWidget* widget = (uiWidget*)frame;
136
137 bool processed = false;
138
139 switch (event->type) {
140 case uiEventMouseButton: {
141 const uiMouseButtonEvent* ev = &event->mouse_button;
142
143 uiMouseButtonState* prev_state = &g_ui.mouse_button_state[ev->button];
144
145 if ((*prev_state == uiMouseDown) && (ev->state == uiMouseUp)) {
146 // Click.
147 uiSendEvent(
148 frame,
149 &(uiInputEvent){
150 .type = uiEventMouseClick,
151 .mouse_click = (uiMouseClickEvent){
152 .button = ev->button, .mouse_position = ev->mouse_position}
153 });
154 }
155
156 *prev_state = ev->state;
157 break;
158 }
159 case uiEventMouseClick: {
160 const uiMouseClickEvent* ev = &event->mouse_click;
161 uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position);
162 if (target) {
163 processed = ProcessClickEvent(target, ev);
164 }
165 break;
166 }
167 case uiEventMouseScroll: {
168 const uiMouseScrollEvent* ev = &event->mouse_scroll;
169 uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position);
170 if (target) {
171 processed = ProcessScrollEvent(target, ev);
172 }
173 break;
174 }
175 }
176
177 return processed;
178}
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 @@
1#include <ui.h>
2
3#include "uiLibrary.h"
4#include "widget/table.h"
5#include "widget/widget.h"
6
7#include <cassert.h>
8
9static void ResizeTable(uiTable* table, int width, int height) {
10 assert(table);
11
12 if (table->cols == 0) {
13 return;
14 }
15
16 // Determine if there is vertical overflow. This determines whether we need to
17 // render a scroll bar, in which case room must be made for it.
18 table->flags.vertical_overflow =
19 (table->rows * g_ui.font->header.glyph_height) >
20 table->widget.rect.height;
21
22 // Surface width: W.
23 // Columns: N
24 //
25 // First, find the minimum width of each column based on their contents.
26 //
27 // If the sum of column widths < N, then distribute the extra space first
28 // among the smallest columns and building up towards the larger.
29 //
30 // If the sum of column widths > N, subtract from the largest column first and
31 // move towards the smaller ones to distribute the space as evenly as
32 // possible.
33
34 // Find the minimum width for each column.
35 int* widths = table->widths;
36 // Header.
37 for (int col = 0; col < table->cols; ++col) {
38 const uiCell* cell = &table->header[col];
39 const uiLabel* label = (uiLabel*)cell->child;
40 const int length = (int)string_length(label->text);
41
42 widths[col] = length;
43 }
44 // Table contents.
45 for (int row = 0; row < table->rows; ++row) {
46 for (int col = 0; col < table->cols; ++col) {
47 const uiCell* cell = GetCell(table, row, col);
48 if (cell->child) {
49 const uiLabel* label = (uiLabel*)cell->child;
50 const int length = (int)string_length(label->text);
51
52 widths[col] = length > widths[col] ? length : widths[col];
53 }
54 }
55 }
56 // Multiply string lengths times glyph width to compute pixel size.
57 for (int col = 0; col < table->cols; ++col) {
58 widths[col] *= g_ui.font->header.glyph_width;
59 }
60
61 // Find the sum of widths.
62 int used_width = 0;
63 for (int col = 0; col < table->cols; ++col) {
64 used_width += widths[col];
65 }
66
67 // Pad if available width is larger than sum of widths.
68 if (used_width < width) {
69 // Divide evenly among columns.
70 // const int extra = width - used_width;
71 // const int pad = extra / table->cols;
72 // const int mod = extra % table->cols;
73 // for (int col = 0; col < table->cols; ++col) {
74 // table->widths[col] += pad + (col < mod ? 1 : 0);
75 // }
76
77 int extra = width - used_width;
78 while (extra > 0) {
79 // Find smallest column.
80 int smallest = 0;
81 for (int col = 1; col < table->cols; ++col) {
82 if (widths[col] < widths[smallest]) {
83 smallest = col;
84 }
85 }
86 // Pad it and subtract from the budget.
87 widths[smallest] += 1;
88 extra--;
89 }
90 }
91 // Shrink if available width is smaller than the sum of widths.
92 else if (used_width > width) {
93 int deficit = used_width - width;
94 while (deficit > 0) {
95 // Find largest column.
96 int largest = 0;
97 for (int col = 1; col < table->cols; ++col) {
98 if (widths[col] > widths[largest]) {
99 largest = col;
100 }
101 }
102 // Shrink it and subtract from the deficit.
103 widths[largest] -= 1;
104 deficit--;
105 }
106 }
107
108 // Now make room for the scroll bar, if necessary.
109 if (table->flags.vertical_overflow) {
110 const int offset = ScrollBarWidth / table->cols;
111 const int remainder = ScrollBarWidth % table->cols;
112 for (int col = 0; col < table->cols; ++col) {
113 table->widths[col] -= offset + (col < remainder ? 1 : 0);
114 assert(table->widths[col] >= 0);
115 }
116 }
117}
118
119static void ResizeWidget(uiWidget* widget, int width, int height) {
120 assert(widget);
121
122 widget->rect.width = width;
123 widget->rect.height = height;
124
125 switch (widget->type) {
126 case uiTypeButton:
127 break;
128 case uiTypeFrame:
129 list_foreach_mut(
130 widget->children, child, { ResizeWidget(child, width, height); });
131 break;
132 case uiTypeLabel:
133 break;
134 case uiTypeTable:
135 ResizeTable((uiTable*)widget, width, height);
136 break;
137 case uiTypeMax:
138 TRAP();
139 break;
140 }
141}
142
143void uiResizeFrame(uiFrame* frame, int width, int height) {
144 assert(frame);
145 ResizeWidget(&frame->widget, width, height);
146}
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 @@
1#include <ui.h>
2
3#include "uiLibrary.h"
4#include "widget/table.h"
5#include "widget/widget.h"
6
7#include <cassert.h>
8#include <cstring.h>
9
10static const uiPixel uiBlack = {40, 40, 40, 255};
11static const uiPixel uiWhite = {255, 255, 255, 255};
12static const uiPixel uiPink = {128, 0, 128, 255};
13
14/// Render state.
15///
16/// Render functions are allowed to manipulate the state internally (e.g., the
17/// subsurface), but must leave the state intact before returning, except, of
18/// course, for the rendered pixels.
19///
20/// We store a subsurface separate from the surface so that we can always check
21/// whether a given coordinate is within the bounds of the physical surface.
22typedef struct RenderState {
23 uiSurface surface; /// Surface of pixels on which the UI is rendered.
24 uiRect subsurface; /// Subregion where the current UI widget is rendered.
25 uiPoint pen; /// Current pen position relative to subsurface.
26} RenderState;
27
28static void RenderWidget(RenderState* state, const uiWidget* widget);
29
30/// Push a new subsurface onto which subsequent UI widgets are rendered.
31void PushSubsurface(
32 RenderState* state, int width, int height, uiRect* original_subsurface,
33 uiPoint* original_pen) {
34 assert(state);
35 assert(original_subsurface);
36 assert(original_pen);
37
38 *original_subsurface = state->subsurface;
39 *original_pen = state->pen;
40
41 state->subsurface.x = state->subsurface.x + state->pen.x;
42 state->subsurface.width = width;
43 state->subsurface.height = height;
44 state->pen.x = 0;
45}
46
47/// Restore the previous subsurface.
48void PopSubsurface(
49 RenderState* state, const uiRect* original_subsurface,
50 const uiPoint* original_pen) {
51 assert(state);
52 assert(original_subsurface);
53 assert(original_pen);
54
55 state->subsurface = *original_subsurface;
56 state->pen = *original_pen;
57}
58
59/// Check whether pen + (w,h) is within the surface and subsurface.
60static bool PenInSurface(const RenderState* state, int w, int h) {
61 assert(state);
62
63 // Surface.
64 const bool in_surface =
65 ((state->subsurface.x + state->pen.x + w) < state->surface.width) &&
66 ((state->subsurface.y + state->pen.y + h) < state->surface.height);
67
68 // Subsurface.
69 const bool in_subsurface = ((state->pen.x + w) < state->subsurface.width) &&
70 ((state->pen.y + h) < state->subsurface.height);
71
72 return in_surface && in_subsurface;
73}
74
75/// Get the pixel at (x,y).
76static uiPixel* SurfaceXy(uiSurface* surface, int x, int y) {
77 assert(surface);
78 assert(x >= 0);
79 assert(y >= 0);
80 assert(x < surface->width);
81 assert(y < surface->height);
82 return surface->pixels + (surface->width * y) + x;
83}
84
85/// Get the pixel at pen + (x,y).
86static uiPixel* PixelXy(RenderState* state, int x, int y) {
87 assert(state);
88 return SurfaceXy(
89 &state->surface, state->subsurface.x + state->pen.x + x,
90 state->subsurface.y + state->pen.y + y);
91}
92
93/// Fill a rectangle with a constant colour.
94static void FillRect(const uiRect* rect, uiPixel colour, RenderState* state) {
95 assert(rect);
96 assert(state);
97 assert(rect->width <= state->subsurface.width);
98 assert(rect->height <= state->subsurface.height);
99
100 for (int y = rect->y; y < rect->y + rect->height; ++y) {
101 uiPixel* pixel = PixelXy(state, rect->x, y);
102 for (int x = rect->x; x < rect->x + rect->width; ++x) {
103 *pixel++ = colour;
104 }
105 }
106}
107
108/// Render a glyph.
109/// The glyph is clamped to the surface's bounds.
110static void RenderGlyph(
111 const FontAtlas* atlas, unsigned char c, RenderState* state) {
112 assert(atlas);
113 assert(state);
114 assert(atlas->header.glyph_width <= state->subsurface.width);
115 assert(atlas->header.glyph_height <= state->subsurface.height);
116
117 const int glyph_width = atlas->header.glyph_width;
118 const int glyph_height = atlas->header.glyph_height;
119
120 const unsigned char* glyph = FontGetGlyph(atlas, c);
121
122 for (int y = 0; (y < atlas->header.glyph_height) &&
123 PenInSurface(state, glyph_width - 1, glyph_height - 1);
124 ++y) {
125 for (int x = 0; (x < atlas->header.glyph_width) &&
126 PenInSurface(state, glyph_width - 1, glyph_height - 1);
127 ++x, ++glyph) {
128 uiPixel* pixel = PixelXy(state, x, y);
129 if (*glyph > 0) {
130 pixel->r = *glyph;
131 pixel->g = *glyph;
132 pixel->b = *glyph;
133 pixel->a = 255;
134 }
135 }
136 }
137}
138
139/// Render text.
140static void RenderText(const char* text, size_t length, RenderState* state) {
141 assert(text);
142 assert(state);
143
144 const FontAtlas* atlas = g_ui.font;
145
146 const int glyph_width = atlas->header.glyph_width;
147 const int glyph_height = atlas->header.glyph_height;
148
149 // Save the x-pen so that we can restore it after rendering the text.
150 const int x0 = state->pen.x;
151
152 // Truncate the text rendering if it exceeds the subsurface's width or height.
153 const char* c = text;
154 for (size_t i = 0;
155 (i < length) && PenInSurface(state, glyph_width - 1, glyph_height - 1);
156 ++i, ++c, state->pen.x += glyph_width) {
157 RenderGlyph(atlas, *c, state);
158 }
159
160 state->pen.x = x0;
161}
162
163/// Render a frame.
164static void RenderFrame(const uiFrame* frame, RenderState* state) {
165 assert(frame);
166
167 FillRect(&frame->widget.rect, uiBlack, state);
168}
169
170/// Render a label.
171static void RenderLabel(const uiLabel* label, RenderState* state) {
172 assert(label);
173 assert(state);
174
175 RenderText(string_data(label->text), string_length(label->text), state);
176}
177
178/// Render a table.
179static void RenderTable(const uiTable* table, RenderState* state) {
180 assert(table);
181 assert(state);
182
183 const int x0 = state->pen.x;
184 const int y0 = state->pen.y;
185
186 uiRect original_subsurface = {0};
187 uiPoint original_pen = {0};
188
189 // Render header.
190 if (table->header) {
191 for (int col = 0; col < table->cols; ++col) {
192 // Crop the column contents to the column width so that one column does
193 // not spill into the next.
194 PushSubsurface(
195 state, table->widths[col], state->subsurface.height,
196 &original_subsurface, &original_pen);
197
198 const uiCell* cell = &table->header[col];
199 RenderWidget(state, cell->child);
200
201 // Reset the original subsurface and pen for subsequent columns.
202 PopSubsurface(state, &original_subsurface, &original_pen);
203
204 // Next column.
205 state->pen.x += table->widths[col];
206 }
207 }
208 state->pen.x = x0;
209 state->pen.y += g_ui.font->header.glyph_height;
210
211 // Render rows.
212 for (int row = table->offset;
213 (row < table->rows) && PenInSurface(state, 0, 0); ++row) {
214 for (int col = 0; (col < table->cols) && PenInSurface(state, 0, 0); ++col) {
215 // Crop the column contents to the column width so that one column does
216 // not spill into the next.
217 PushSubsurface(
218 state, table->widths[col], state->subsurface.height,
219 &original_subsurface, &original_pen);
220
221 state->subsurface.x = state->subsurface.x + state->pen.x;
222 state->subsurface.width = table->widths[col];
223 state->pen.x = 0;
224
225 const uiCell* cell = GetCell(table, row, col);
226 RenderWidget(state, cell->child);
227
228 // Reset the original subsurface and pen for subsequent columns.
229 PopSubsurface(state, &original_subsurface, &original_pen);
230
231 // Next column.
232 state->pen.x += table->widths[col];
233 }
234 state->pen.x = x0;
235 state->pen.y += g_ui.font->header.glyph_height;
236 }
237 state->pen.y = y0;
238}
239
240/// Render a widget.
241static void RenderWidget(RenderState* state, const uiWidget* widget) {
242 assert(state);
243 assert(widget);
244
245 // Render this widget.
246 switch (widget->type) {
247 case uiTypeButton:
248 break;
249 case uiTypeFrame:
250 RenderFrame((const uiFrame*)widget, state);
251 break;
252 case uiTypeLabel:
253 RenderLabel((const uiLabel*)widget, state);
254 break;
255 case uiTypeTable:
256 RenderTable((const uiTable*)widget, state);
257 break;
258 case uiTypeMax:
259 TRAP();
260 break;
261 }
262
263 // Render children.
264 list_foreach(widget->children, child, { RenderWidget(state, child); });
265}
266
267void uiRender(const uiFrame* frame, uiSurface* surface) {
268 assert(frame);
269 assert(surface);
270
271 RenderWidget(
272 &(RenderState){
273 .surface = *surface,
274 .subsurface =
275 (uiRect){
276 .x = 0,
277 .y = 0,
278 .width = surface->width,
279 .height = surface->height},
280 .pen = {.x = 0, .y = 0},
281 },
282 (const uiWidget*)frame);
283}
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 @@
1#include <ui.h> 1#include <ui.h>
2 2
3#include <cassert.h> 3#include "uiLibrary.h"
4#include <cstring.h> 4#include "widget/widget.h"
5#include <font.h>
6#include <list.h>
7
8#include <stdlib.h>
9
10#define Max(a, b) ((a) > (b) ? (a) : (b))
11
12#define MaxWidgetEvents 8
13
14static void* uiAlloc(size_t count, size_t size) {
15 void* mem = calloc(count, size);
16 ASSERT(mem);
17 return mem;
18}
19
20#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE))
21#define UI_DEL(ppWidget) \
22 { \
23 assert(ppWidget); \
24 void* widget_ = *ppWidget; \
25 if (widget_) { \
26 free(widget_); \
27 *ppWidget = 0; \
28 } \
29 }
30
31DEF_LIST(Widget, uiWidget*)
32
33/// Base widget type.
34typedef struct uiWidget {
35 uiWidgetType type;
36 uiRect rect;
37 Widget_list children;
38} uiWidget;
39
40/// Button.
41typedef struct uiButton {
42 uiWidget widget;
43 string text;
44} uiButton;
45
46/// Frame.
47typedef struct uiFrame {
48 uiWidget widget;
49} uiFrame;
50
51/// Label.
52typedef struct uiLabel {
53 uiWidget widget;
54 string text;
55} uiLabel;
56
57/// Table cell.
58typedef struct uiCell {
59 uiWidget* child;
60} uiCell;
61
62/// Table.
63typedef struct uiTable {
64 uiWidget widget;
65 int rows;
66 int cols;
67 int* widths; // Width, in pixels, for each column.
68 uiCell* header; // If non-null, row of 'cols' header cells.
69 uiCell** cells; // Array of 'rows' rows, each of 'cols' cells.
70 int offset; // Offset into the rows of the table. Units: rows.
71} uiTable;
72
73typedef struct uiLibrary {
74 FontAtlas* font;
75 uiMouseButtonState mouse_button_state[uiMouseButtonMax];
76 uiWidgetEvent widget_events[MaxWidgetEvents];
77 int num_widget_events;
78} uiLibrary;
79 5
80// ----------------------------------------------------------------------------- 6// -----------------------------------------------------------------------------
81// Library. 7// Library.
82 8
83uiLibrary g_ui = {0};
84
85bool uiInit(void) { 9bool uiInit(void) {
86 // TODO: Embed the font into the library instead. 10 // TODO: Embed the font into the library instead.
87 const char* font_path = "../ui/fontbaker/NK57.bin"; 11 const char* font_path = "../ui/fontbaker/NK57.bin";
@@ -105,829 +29,3 @@ bool uiInit(void) {
105} 29}
106 30
107void uiShutdown(void) {} 31void uiShutdown(void) {}
108
109// -----------------------------------------------------------------------------
110// Widget pointers.
111
112uiPtr uiMakeButtonPtr(uiButton* button) {
113 assert(button);
114 return (uiPtr){.type = uiTypeButton, .button = button};
115}
116
117uiPtr uiMakeFramePtr(uiFrame* frame) {
118 assert(frame);
119 return (uiPtr){.type = uiTypeFrame, .frame = frame};
120}
121
122uiPtr uiMakeLabelPtr(uiLabel* label) {
123 assert(label);
124 return (uiPtr){.type = uiTypeLabel, .label = label};
125}
126
127uiPtr uiMakeTablePtr(uiTable* table) {
128 assert(table);
129 return (uiPtr){.type = uiTypeTable, .table = table};
130}
131
132static uiPtr uiMakeWidgetPtr(uiWidget* widget) {
133 assert(widget);
134 return (uiPtr){.type = widget->type, .widget = widget};
135}
136
137uiButton* uiGetButtonPtr(uiPtr ptr) {
138 assert(ptr.type == uiTypeButton);
139 assert(ptr.button);
140 return ptr.button;
141}
142
143uiFrame* uiGetFramePtr(uiPtr ptr) {
144 assert(ptr.type == uiTypeFrame);
145 assert(ptr.frame);
146 return ptr.frame;
147}
148
149uiLabel* uiGetLabelPtr(uiPtr ptr) {
150 assert(ptr.type == uiTypeLabel);
151 assert(ptr.label);
152 return ptr.label;
153}
154
155uiTable* uiGetTablePtr(uiPtr ptr) {
156 assert(ptr.type == uiTypeTable);
157 assert(ptr.table);
158 return ptr.table;
159}
160
161// -----------------------------------------------------------------------------
162// Widget.
163
164uiWidgetType uiWidgetGetType(const uiWidget* widget) {
165 assert(widget);
166 return widget->type;
167}
168
169static void DestroyWidget(uiWidget** ppWidget) {
170 assert(ppWidget);
171
172 uiWidget* widget = *ppWidget;
173 if (widget) {
174 list_foreach_mut(widget->children, child, { DestroyWidget(&child); });
175 }
176 UI_DEL(ppWidget);
177}
178
179void uiWidgetSetParent(uiPtr child_, uiPtr parent_) {
180 uiWidget* child = child_.widget;
181 uiWidget* parent = parent_.widget;
182
183 assert(child);
184 assert(parent);
185
186 list_add(parent->children, child);
187}
188
189// -----------------------------------------------------------------------------
190// Button.
191
192uiButton* uiMakeButton(const char* text) {
193 assert(text);
194
195 uiButton* button = UI_NEW(uiButton);
196
197 *button = (uiButton){
198 .widget =
199 (uiWidget){
200 .type = uiTypeButton,
201 .rect = {0},
202 },
203 .text = string_new(text),
204 };
205 return button;
206}
207
208// -----------------------------------------------------------------------------
209// Label.
210
211uiLabel* uiMakeLabel(const char* text) {
212 assert(text);
213
214 uiLabel* label = UI_NEW(uiLabel);
215
216 *label = (uiLabel){
217 .widget =
218 (uiWidget){
219 .type = uiTypeLabel,
220 .rect =
221 (uiRect){
222 .width =
223 (int)strlen(text) * g_ui.font->header.glyph_width,
224 .height = g_ui.font->header.glyph_height}},
225 .text = string_new(text),
226 };
227 return label;
228}
229
230const char* uiLabelGetText(const uiLabel* label) {
231 assert(label);
232 return string_data(label->text);
233}
234
235// -----------------------------------------------------------------------------
236// Frame.
237
238uiFrame* uiMakeFrame(void) {
239 uiFrame* frame = UI_NEW(uiFrame);
240 frame->widget.type = uiTypeFrame;
241 return frame;
242}
243
244void uiDestroyFrame(uiFrame** ppFrame) { DestroyWidget((uiWidget**)ppFrame); }
245
246uiSize uiGetFrameSize(const uiFrame* frame) {
247 assert(frame);
248 return (uiSize){
249 .width = frame->widget.rect.width,
250 .height = frame->widget.rect.height,
251 };
252}
253
254// -----------------------------------------------------------------------------
255// Table.
256
257static const uiCell* GetCell(const uiTable* table, int row, int col) {
258 assert(table);
259 return &table->cells[row][col];
260}
261
262static uiCell* GetCellMut(uiTable* table, int row, int col) {
263 assert(table);
264 return (uiCell*)GetCell(table, row, col);
265}
266
267static uiCell** GetLastRow(uiTable* table) {
268 assert(table);
269 assert(table->rows > 0);
270 return &table->cells[table->rows - 1];
271}
272
273uiTable* uiMakeTable(int rows, int cols, const char** header) {
274 uiTable* table = UI_NEW(uiTable);
275
276 *table = (uiTable){
277 .widget = (uiWidget){.type = uiTypeTable},
278 .rows = rows,
279 .cols = cols,
280 .widths = (cols > 0) ? calloc(cols, sizeof(int)) : 0,
281 .header = header ? calloc(cols, sizeof(uiCell)) : 0,
282 .cells = (rows * cols > 0) ? calloc(rows, sizeof(uiCell*)) : 0,
283 };
284
285 if (header) {
286 for (int col = 0; col < cols; ++col) {
287 table->header[col].child = (uiWidget*)uiMakeLabel(header[col]);
288 }
289 }
290
291 return table;
292}
293
294void 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
318void uiTableAddRow(uiTable* table, const char** row) {
319 assert(table);
320
321 table->rows++;
322
323 uiCell** cells = realloc(table->cells, table->rows * sizeof(uiCell*));
324 ASSERT(cells);
325 table->cells = cells;
326
327 uiCell** pLastRow = GetLastRow(table);
328 *pLastRow = calloc(table->cols, sizeof(uiCell));
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]);
334 }
335}
336
337void uiTableSet(uiTable* table, int row, int col, uiPtr child) {
338 assert(table);
339 assert(child.widget);
340
341 GetCellMut(table, row, col)->child = child.widget;
342}
343
344const uiWidget* uiTableGet(const uiTable* table, int row, int col) {
345 assert(table);
346 return GetCell(table, row, col)->child;
347}
348
349uiWidget* uiTableGetMut(uiTable* table, int row, int col) {
350 assert(table);
351 return GetCellMut(table, row, col)->child;
352}
353
354// -----------------------------------------------------------------------------
355// Layout and resizing.
356
357static void ResizeTable(uiTable* table, int width, int height) {
358 assert(table);
359
360 if (table->cols == 0) {
361 return;
362 }
363
364 // Surface width: W.
365 // Columns: N
366 //
367 // First, find the minimum width of each column based on their contents.
368 //
369 // If the sum of column widths < N, then distribute the extra space first
370 // among the smallest columns and building up towards the larger.
371 //
372 // If the sum of column widths > N, subtract from the largest column first and
373 // move towards the smaller ones to distribute the space as evenly as
374 // possible.
375
376 // Find the minimum width for each column.
377 int* widths = table->widths;
378 // Header.
379 for (int col = 0; col < table->cols; ++col) {
380 const uiCell* cell = &table->header[col];
381 const uiLabel* label = (uiLabel*)cell->child;
382 const int length = (int)string_length(label->text);
383
384 widths[col] = length;
385 }
386 // Table contents.
387 for (int row = 0; row < table->rows; ++row) {
388 for (int col = 0; col < table->cols; ++col) {
389 const uiCell* cell = GetCell(table, row, col);
390 if (cell->child) {
391 const uiLabel* label = (uiLabel*)cell->child;
392 const int length = (int)string_length(label->text);
393
394 widths[col] = length > widths[col] ? length : widths[col];
395 }
396 }
397 }
398 // Multiply string lengths times glyph width to compute pixel size.
399 for (int col = 0; col < table->cols; ++col) {
400 widths[col] *= g_ui.font->header.glyph_width;
401 }
402
403 // Find the sum of widths.
404 int used_width = 0;
405 for (int col = 0; col < table->cols; ++col) {
406 used_width += widths[col];
407 }
408
409 // Pad if available width is larger than sum of widths.
410 if (used_width < width) {
411 // Divide evenly among columns.
412 // const int extra = width - used_width;
413 // const int pad = extra / table->cols;
414 // const int mod = extra % table->cols;
415 // for (int col = 0; col < table->cols; ++col) {
416 // table->widths[col] += pad + (col < mod ? 1 : 0);
417 // }
418
419 int extra = width - used_width;
420 while (extra > 0) {
421 // Find smallest column.
422 int smallest = 0;
423 for (int col = 1; col < table->cols; ++col) {
424 if (widths[col] < widths[smallest]) {
425 smallest = col;
426 }
427 }
428 // Pad it and subtract from the budget.
429 widths[smallest] += 1;
430 extra--;
431 }
432 }
433 // Shrink if available width is smaller than the sum of widths.
434 else if (used_width > width) {
435 int deficit = used_width - width;
436 while (deficit > 0) {
437 // Find largest column.
438 int largest = 0;
439 for (int col = 1; col < table->cols; ++col) {
440 if (widths[col] > widths[largest]) {
441 largest = col;
442 }
443 }
444 // Shrink it and subtract from the deficit.
445 widths[largest] -= 1;
446 deficit--;
447 }
448 }
449}
450
451static void ResizeWidget(uiWidget* widget, int width, int height) {
452 assert(widget);
453
454 widget->rect.width = width;
455 widget->rect.height = height;
456
457 switch (widget->type) {
458 case uiTypeButton:
459 break;
460 case uiTypeFrame:
461 list_foreach_mut(
462 widget->children, child, { ResizeWidget(child, width, height); });
463 break;
464 case uiTypeLabel:
465 break;
466 case uiTypeTable:
467 ResizeTable((uiTable*)widget, width, height);
468 break;
469 case uiTypeMax:
470 TRAP();
471 break;
472 }
473}
474
475void uiResizeFrame(uiFrame* frame, int width, int height) {
476 assert(frame);
477 ResizeWidget(&frame->widget, width, height);
478}
479
480// -----------------------------------------------------------------------------
481// Rendering.
482
483static const uiPixel uiBlack = {40, 40, 40, 255};
484static const uiPixel uiWhite = {255, 255, 255, 255};
485static const uiPixel uiPink = {128, 0, 128, 255};
486
487/// Render state.
488///
489/// Render functions are allowed to manipulate the state internally (e.g., the
490/// subsurface), but must leave the state intact before returning, except, of
491/// course, for the rendered pixels.
492///
493/// We store a subsurface separate from the surface so that we can always check
494/// whether a given coordinate is within the bounds of the physical surface.
495typedef struct RenderState {
496 uiSurface surface; /// Surface of pixels on which the UI is rendered.
497 uiRect subsurface; /// Subregion where the current UI widget is rendered.
498 uiPoint pen; /// Current pen position relative to subsurface.
499} RenderState;
500
501static void RenderWidget(RenderState* state, const uiWidget* widget);
502
503void PushSubsurface(
504 RenderState* state, int width, int height, uiRect* original_subsurface,
505 uiPoint* original_pen) {
506 assert(state);
507 assert(original_subsurface);
508 assert(original_pen);
509
510 *original_subsurface = state->subsurface;
511 *original_pen = state->pen;
512
513 state->subsurface.x = state->subsurface.x + state->pen.x;
514 state->subsurface.width = width;
515 state->subsurface.height = height;
516 state->pen.x = 0;
517}
518
519void PopSubsurface(
520 RenderState* state, const uiRect* original_subsurface,
521 const uiPoint* original_pen) {
522 assert(state);
523 assert(original_subsurface);
524 assert(original_pen);
525
526 state->subsurface = *original_subsurface;
527 state->pen = *original_pen;
528}
529
530/// Checks whether pen + (w,h) is within the surface and subsurface.
531static bool PenInSurface(const RenderState* state, int w, int h) {
532 assert(state);
533
534 // Surface.
535 const bool in_surface =
536 ((state->subsurface.x + state->pen.x + w) < state->surface.width) &&
537 ((state->subsurface.y + state->pen.y + h) < state->surface.height);
538
539 // Subsurface.
540 const bool in_subsurface = ((state->pen.x + w) < state->subsurface.width) &&
541 ((state->pen.y + h) < state->subsurface.height);
542
543 return in_surface && in_subsurface;
544}
545
546/// Get the pixel at (x,y).
547static uiPixel* SurfaceXy(uiSurface* surface, int x, int y) {
548 assert(surface);
549 assert(x >= 0);
550 assert(y >= 0);
551 assert(x < surface->width);
552 assert(y < surface->height);
553 return surface->pixels + (surface->width * y) + x;
554}
555
556/// Get the pixel at pen + (x,y).
557static uiPixel* PixelXy(RenderState* state, int x, int y) {
558 assert(state);
559 return SurfaceXy(
560 &state->surface, state->subsurface.x + state->pen.x + x,
561 state->subsurface.y + state->pen.y + y);
562}
563
564static void FillRect(const uiRect* rect, uiPixel colour, RenderState* state) {
565 assert(rect);
566 assert(state);
567 assert(rect->width <= state->subsurface.width);
568 assert(rect->height <= state->subsurface.height);
569
570 for (int y = rect->y; y < rect->y + rect->height; ++y) {
571 uiPixel* pixel = PixelXy(state, rect->x, y);
572 for (int x = rect->x; x < rect->x + rect->width; ++x) {
573 *pixel++ = colour;
574 }
575 }
576}
577
578/// Render a glyph.
579/// The glyph is clamped to the surface's bounds.
580static void RenderGlyph(
581 const FontAtlas* atlas, unsigned char c, RenderState* state) {
582 assert(atlas);
583 assert(state);
584 assert(atlas->header.glyph_width <= state->subsurface.width);
585 assert(atlas->header.glyph_height <= state->subsurface.height);
586
587 const int glyph_width = atlas->header.glyph_width;
588 const int glyph_height = atlas->header.glyph_height;
589
590 const unsigned char* glyph = FontGetGlyph(atlas, c);
591
592 for (int y = 0; (y < atlas->header.glyph_height) &&
593 PenInSurface(state, glyph_width - 1, glyph_height - 1);
594 ++y) {
595 for (int x = 0; (x < atlas->header.glyph_width) &&
596 PenInSurface(state, glyph_width - 1, glyph_height - 1);
597 ++x, ++glyph) {
598 uiPixel* pixel = PixelXy(state, x, y);
599 if (*glyph > 0) {
600 pixel->r = *glyph;
601 pixel->g = *glyph;
602 pixel->b = *glyph;
603 pixel->a = 255;
604 }
605 }
606 }
607}
608
609static void RenderText(const char* text, size_t length, RenderState* state) {
610 assert(text);
611 assert(state);
612
613 const FontAtlas* atlas = g_ui.font;
614
615 const int glyph_width = atlas->header.glyph_width;
616 const int glyph_height = atlas->header.glyph_height;
617
618 // Save the x-pen so that we can restore it after rendering the text.
619 const int x0 = state->pen.x;
620
621 // Truncate the text rendering if it exceeds the subsurface's width or height.
622 const char* c = text;
623 for (size_t i = 0;
624 (i < length) && PenInSurface(state, glyph_width - 1, glyph_height - 1);
625 ++i, ++c, state->pen.x += glyph_width) {
626 RenderGlyph(atlas, *c, state);
627 }
628
629 state->pen.x = x0;
630}
631
632static void RenderFrame(const uiFrame* frame, RenderState* state) {
633 assert(frame);
634
635 FillRect(&frame->widget.rect, uiBlack, state);
636}
637
638static void RenderLabel(const uiLabel* label, RenderState* state) {
639 assert(label);
640 assert(state);
641
642 RenderText(string_data(label->text), string_length(label->text), state);
643}
644
645static void RenderTable(const uiTable* table, RenderState* state) {
646 assert(table);
647 assert(state);
648
649 const int x0 = state->pen.x;
650 const int y0 = state->pen.y;
651
652 uiRect original_subsurface = {0};
653 uiPoint original_pen = {0};
654
655 // Render header.
656 if (table->header) {
657 for (int col = 0; col < table->cols; ++col) {
658 // Crop the column contents to the column width so that one column does
659 // not spill into the next.
660 PushSubsurface(
661 state, table->widths[col], state->subsurface.height,
662 &original_subsurface, &original_pen);
663
664 const uiCell* cell = &table->header[col];
665 RenderWidget(state, cell->child);
666
667 // Reset the original subsurface and pen for subsequent columns.
668 PopSubsurface(state, &original_subsurface, &original_pen);
669
670 // Next column.
671 state->pen.x += table->widths[col];
672 }
673 }
674 state->pen.x = x0;
675 state->pen.y += g_ui.font->header.glyph_height;
676
677 // Render rows.
678 for (int row = table->offset;
679 (row < table->rows) && PenInSurface(state, 0, 0); ++row) {
680 for (int col = 0; (col < table->cols) && PenInSurface(state, 0, 0); ++col) {
681 // Crop the column contents to the column width so that one column does
682 // not spill into the next.
683 PushSubsurface(
684 state, table->widths[col], state->subsurface.height,
685 &original_subsurface, &original_pen);
686
687 state->subsurface.x = state->subsurface.x + state->pen.x;
688 state->subsurface.width = table->widths[col];
689 state->pen.x = 0;
690
691 const uiCell* cell = GetCell(table, row, col);
692 RenderWidget(state, cell->child);
693
694 // Reset the original subsurface and pen for subsequent columns.
695 PopSubsurface(state, &original_subsurface, &original_pen);
696
697 // Next column.
698 state->pen.x += table->widths[col];
699 }
700 state->pen.x = x0;
701 state->pen.y += g_ui.font->header.glyph_height;
702 }
703 state->pen.y = y0;
704}
705
706static void RenderWidget(RenderState* state, const uiWidget* widget) {
707 assert(state);
708 assert(widget);
709
710 // Render this widget.
711 switch (widget->type) {
712 case uiTypeButton:
713 break;
714 case uiTypeFrame:
715 RenderFrame((const uiFrame*)widget, state);
716 break;
717 case uiTypeLabel:
718 RenderLabel((const uiLabel*)widget, state);
719 break;
720 case uiTypeTable:
721 RenderTable((const uiTable*)widget, state);
722 break;
723 case uiTypeMax:
724 TRAP();
725 break;
726 }
727
728 // Render children.
729 list_foreach(widget->children, child, { RenderWidget(state, child); });
730}
731
732void uiRender(const uiFrame* frame, uiSurface* surface) {
733 assert(frame);
734 assert(surface);
735
736 RenderWidget(
737 &(RenderState){
738 .surface = *surface,
739 .subsurface =
740 (uiRect){
741 .x = 0,
742 .y = 0,
743 .width = surface->width,
744 .height = surface->height},
745 .pen = {.x = 0, .y = 0},
746 },
747 (const uiWidget*)frame);
748}
749
750// -----------------------------------------------------------------------------
751// UI Events.
752
753static 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
760int 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
773static 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
778static 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
797static 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
826static 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
842static 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
848static 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
867static 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
886bool 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}
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 @@
1#include "uiLibrary.h"
2
3uiLibrary 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 @@
1#pragma once
2
3#include <ui.h>
4
5#include "constants.h"
6
7#include <font.h>
8
9typedef struct uiLibrary {
10 FontAtlas* font;
11 uiMouseButtonState mouse_button_state[uiMouseButtonMax];
12 uiWidgetEvent widget_events[MaxWidgetEvents];
13 int num_widget_events;
14} uiLibrary;
15
16extern 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 @@
1#include <ui.h>
2
3#include "widget.h"
4
5uiButton* uiMakeButton(const char* text) {
6 assert(text);
7
8 uiButton* button = UI_NEW(uiButton);
9
10 *button = (uiButton){
11 .widget =
12 (uiWidget){
13 .type = uiTypeButton,
14 .rect = {0},
15 },
16 .text = string_new(text),
17 };
18 return button;
19}
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 @@
1#include <ui.h>
2
3#include "widget.h"
4
5uiFrame* uiMakeFrame(void) {
6 uiFrame* frame = UI_NEW(uiFrame);
7 frame->widget.type = uiTypeFrame;
8 return frame;
9}
10
11void uiDestroyFrame(uiFrame** ppFrame) { DestroyWidget((uiWidget**)ppFrame); }
12
13uiSize uiGetFrameSize(const uiFrame* frame) {
14 assert(frame);
15 return (uiSize){
16 .width = frame->widget.rect.width,
17 .height = frame->widget.rect.height,
18 };
19}
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 @@
1#include <ui.h>
2
3#include "uiLibrary.h"
4#include "widget.h"
5
6uiLabel* uiMakeLabel(const char* text) {
7 assert(text);
8
9 uiLabel* label = UI_NEW(uiLabel);
10
11 *label = (uiLabel){
12 .widget =
13 (uiWidget){
14 .type = uiTypeLabel,
15 .rect =
16 (uiRect){
17 .width =
18 (int)strlen(text) * g_ui.font->header.glyph_width,
19 .height = g_ui.font->header.glyph_height}},
20 .text = string_new(text),
21 };
22 return label;
23}
24
25const char* uiLabelGetText(const uiLabel* label) {
26 assert(label);
27 return string_data(label->text);
28}
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 @@
1#include "table.h"
2
3#include "widget.h"
4
5const uiCell* GetCell(const uiTable* table, int row, int col) {
6 assert(table);
7 return &table->cells[row][col];
8}
9
10uiCell* GetCellMut(uiTable* table, int row, int col) {
11 assert(table);
12 return (uiCell*)GetCell(table, row, col);
13}
14
15uiCell** GetLastRow(uiTable* table) {
16 assert(table);
17 assert(table->rows > 0);
18 return &table->cells[table->rows - 1];
19}
20
21uiTable* uiMakeTable(int rows, int cols, const char** header) {
22 uiTable* table = UI_NEW(uiTable);
23
24 *table = (uiTable){
25 .widget = (uiWidget){.type = uiTypeTable},
26 .rows = rows,
27 .cols = cols,
28 .widths = (cols > 0) ? calloc(cols, sizeof(int)) : 0,
29 .header = header ? calloc(cols, sizeof(uiCell)) : 0,
30 .cells = (rows * cols > 0) ? calloc(rows, sizeof(uiCell*)) : 0,
31 .flags = {0},
32 };
33
34 if (header) {
35 for (int col = 0; col < cols; ++col) {
36 table->header[col].child = (uiWidget*)uiMakeLabel(header[col]);
37 }
38 }
39
40 return table;
41}
42
43void uiTableClear(uiTable* table) {
44 assert(table);
45
46 // Free row data.
47 if (table->cells) {
48 for (int row = 0; row < table->rows; ++row) {
49 for (int col = 0; col < table->cols; ++col) {
50 DestroyWidget(&table->cells[row][col].child);
51 }
52 free(table->cells[row]);
53 }
54 free(table->cells);
55 table->cells = 0;
56 }
57 table->rows = 0;
58
59 // Clear row widths.
60 for (int i = 0; i < table->cols; ++i) {
61 table->widths[i] = 0;
62 }
63
64 table->offset = 0;
65
66 table->flags.vertical_overflow = 0;
67}
68
69void uiTableAddRow(uiTable* table, const char** row) {
70 assert(table);
71
72 table->rows++;
73
74 uiCell** cells = realloc(table->cells, table->rows * sizeof(uiCell*));
75 ASSERT(cells);
76 table->cells = cells;
77
78 uiCell** pLastRow = GetLastRow(table);
79 *pLastRow = calloc(table->cols, sizeof(uiCell));
80 ASSERT(*pLastRow);
81 uiCell* lastRow = *pLastRow;
82
83 for (int col = 0; col < table->cols; ++col) {
84 lastRow[col].child = (uiWidget*)uiMakeLabel(row[col]);
85 }
86}
87
88void uiTableSet(uiTable* table, int row, int col, uiPtr child) {
89 assert(table);
90 assert(child.widget);
91
92 GetCellMut(table, row, col)->child = child.widget;
93}
94
95const uiWidget* uiTableGet(const uiTable* table, int row, int col) {
96 assert(table);
97 return GetCell(table, row, col)->child;
98}
99
100uiWidget* uiTableGetMut(uiTable* table, int row, int col) {
101 assert(table);
102 return GetCellMut(table, row, col)->child;
103}
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 @@
1#pragma once
2
3#include <ui.h>
4
5#include "widget.h"
6
7const uiCell* GetCell(const uiTable* table, int row, int col);
8
9uiCell* GetCellMut(uiTable* table, int row, int col);
10
11uiCell** 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 @@
1#include "widget.h"
2
3#include <cassert.h>
4
5// -----------------------------------------------------------------------------
6// Widget.
7
8#define UI_DEL(ppWidget) \
9 { \
10 assert(ppWidget); \
11 void* widget_ = *ppWidget; \
12 if (widget_) { \
13 free(widget_); \
14 *ppWidget = 0; \
15 } \
16 }
17
18uiWidgetType uiWidgetGetType(const uiWidget* widget) {
19 assert(widget);
20 return widget->type;
21}
22
23void DestroyWidget(uiWidget** ppWidget) {
24 assert(ppWidget);
25
26 uiWidget* widget = *ppWidget;
27 if (widget) {
28 list_foreach_mut(widget->children, child, { DestroyWidget(&child); });
29 }
30 UI_DEL(ppWidget);
31}
32
33void uiWidgetSetParent(uiPtr child_, uiPtr parent_) {
34 uiWidget* child = child_.widget;
35 uiWidget* parent = parent_.widget;
36
37 assert(child);
38 assert(parent);
39
40 list_add(parent->children, child);
41}
42
43// -----------------------------------------------------------------------------
44// Widget pointers.
45
46uiPtr uiMakeButtonPtr(uiButton* button) {
47 assert(button);
48 return (uiPtr){.type = uiTypeButton, .button = button};
49}
50
51uiPtr uiMakeFramePtr(uiFrame* frame) {
52 assert(frame);
53 return (uiPtr){.type = uiTypeFrame, .frame = frame};
54}
55
56uiPtr uiMakeLabelPtr(uiLabel* label) {
57 assert(label);
58 return (uiPtr){.type = uiTypeLabel, .label = label};
59}
60
61uiPtr uiMakeTablePtr(uiTable* table) {
62 assert(table);
63 return (uiPtr){.type = uiTypeTable, .table = table};
64}
65
66static uiPtr uiMakeWidgetPtr(uiWidget* widget) {
67 assert(widget);
68 return (uiPtr){.type = widget->type, .widget = widget};
69}
70
71uiButton* uiGetButtonPtr(uiPtr ptr) {
72 assert(ptr.type == uiTypeButton);
73 assert(ptr.button);
74 return ptr.button;
75}
76
77uiFrame* uiGetFramePtr(uiPtr ptr) {
78 assert(ptr.type == uiTypeFrame);
79 assert(ptr.frame);
80 return ptr.frame;
81}
82
83uiLabel* uiGetLabelPtr(uiPtr ptr) {
84 assert(ptr.type == uiTypeLabel);
85 assert(ptr.label);
86 return ptr.label;
87}
88
89uiTable* uiGetTablePtr(uiPtr ptr) {
90 assert(ptr.type == uiTypeTable);
91 assert(ptr.table);
92 return ptr.table;
93}
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 @@
1#pragma once
2
3#include <ui.h>
4
5#include <cstring.h>
6#include <list.h>
7
8#include <stdbool.h>
9
10DEF_LIST(Widget, uiWidget*)
11
12#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE))
13
14static inline void* uiAlloc(size_t count, size_t size) {
15 void* mem = calloc(count, size);
16 ASSERT(mem);
17 return mem;
18}
19
20// -----------------------------------------------------------------------------
21// Widgets.
22
23/// Base widget type.
24typedef struct uiWidget {
25 uiWidgetType type;
26 uiRect rect;
27 Widget_list children;
28} uiWidget;
29
30/// Button.
31typedef struct uiButton {
32 uiWidget widget;
33 string text;
34} uiButton;
35
36/// Frame.
37typedef struct uiFrame {
38 uiWidget widget;
39} uiFrame;
40
41/// Label.
42typedef struct uiLabel {
43 uiWidget widget;
44 string text;
45} uiLabel;
46
47/// Table cell.
48typedef struct uiCell {
49 uiWidget* child;
50} uiCell;
51
52/// Table.
53typedef struct uiTable {
54 uiWidget widget;
55 int rows;
56 int cols;
57 int* widths; // Width, in pixels, for each column.
58 uiCell* header; // If non-null, row of 'cols' header cells.
59 uiCell** cells; // Array of 'rows' rows, each of 'cols' cells.
60 int offset; // Offset into the rows of the table. Units: rows.
61 struct {
62 bool vertical_overflow : 1; // True if contents overflow vertically.
63 } flags;
64} uiTable;
65
66void DestroyWidget(uiWidget** ppWidget);