summaryrefslogtreecommitdiff
path: root/src/ui.c
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2024-05-04 16:44:28 -0700
committer3gg <3gg@shellblade.net>2024-05-04 16:52:53 -0700
commitaf641426fad35cd857c1f14bda523db3d85a70cd (patch)
tree8a219b03aef0c80cac56cd6b88571a7a6988b35b /src/ui.c
Initial commit.
Diffstat (limited to 'src/ui.c')
-rw-r--r--src/ui.c690
1 files changed, 690 insertions, 0 deletions
diff --git a/src/ui.c b/src/ui.c
new file mode 100644
index 0000000..a5ab8d3
--- /dev/null
+++ b/src/ui.c
@@ -0,0 +1,690 @@
1#include <ui.h>
2
3#include <cassert.h>
4#include <cstring.h>
5#include <font.h>
6#include <list.h>
7
8#include <stdlib.h>
9
10static void* uiAlloc(size_t count, size_t size) {
11 void* mem = calloc(count, size);
12 ASSERT(mem);
13 return mem;
14}
15
16#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE))
17#define UI_DEL(ppWidget) \
18 { \
19 assert(ppWidget); \
20 void* widget_ = *ppWidget; \
21 if (widget_) { \
22 free(widget_); \
23 *ppWidget = 0; \
24 } \
25 }
26
27DEF_LIST(Widget, uiWidget*)
28
29/// Base widget type.
30typedef struct uiWidget {
31 uiWidgetType type;
32 uiRect rect;
33 Widget_list children;
34} uiWidget;
35
36/// Button.
37typedef struct uiButton {
38 uiWidget widget;
39 string text;
40} uiButton;
41
42/// Frame.
43typedef struct uiFrame {
44 uiWidget widget;
45} uiFrame;
46
47/// Label.
48typedef struct uiLabel {
49 uiWidget widget;
50 string text;
51} uiLabel;
52
53/// Table cell.
54typedef struct uiCell {
55 uiWidget* child;
56} uiCell;
57
58/// Table.
59typedef struct uiTable {
60 uiWidget widget;
61 int rows;
62 int cols;
63 int* widths; /// Width, in pixels, for each each column.
64 uiCell* header; /// If non-null, row of 'cols' header cells.
65 uiCell* cells; /// Array of 'rows * cols' cells.
66} uiTable;
67
68typedef struct uiLibrary {
69 FontAtlas* font;
70} uiLibrary;
71
72// -----------------------------------------------------------------------------
73// Library.
74
75uiLibrary g_ui = {0};
76
77bool uiInit(void) {
78 // TODO: Embed the font into the library instead.
79 const char* font_path = "../ui/fontbaker/NK57.bin";
80 if (!(g_ui.font = LoadFontAtlas(font_path))) {
81 return false;
82 }
83
84 // TODO: Remove.
85 const FontHeader* header = &g_ui.font->header;
86 const int glyph_size = header->glyph_width * header->glyph_height;
87 const int atlas_size = header->num_glyphs * glyph_size;
88 printf("Loaded font: %s\n", font_path);
89 printf(
90 "Glyph: %dx%d (%d bytes)\n", header->glyph_width, header->glyph_height,
91 glyph_size);
92 printf(
93 "Atlas: %dx%d (%d bytes)\n", header->num_glyphs * header->glyph_width,
94 header->glyph_height, atlas_size);
95
96 return true;
97}
98
99void uiShutdown(void) {}
100
101// -----------------------------------------------------------------------------
102// Widget.
103
104static uiButton* uiGetButtonPtr(uiWidgetPtr ptr) {
105 assert(ptr.type == uiTypeButton);
106 assert(ptr.button);
107 return ptr.button;
108}
109
110static uiFrame* uiGetFramePtr(uiWidgetPtr ptr) {
111 assert(ptr.type == uiTypeFrame);
112 assert(ptr.frame);
113 return ptr.frame;
114}
115
116static uiLabel* uiGetLabelPtr(uiWidgetPtr ptr) {
117 assert(ptr.type == uiTypeLabel);
118 assert(ptr.label);
119 return ptr.label;
120}
121
122static uiTable* uiGetTablePtr(uiWidgetPtr ptr) {
123 assert(ptr.type == uiTypeTable);
124 assert(ptr.table);
125 return ptr.table;
126}
127
128static void DestroyWidget(uiWidget** ppWidget) {
129 assert(ppWidget);
130
131 uiWidget* widget = *ppWidget;
132 if (widget) {
133 list_foreach_mut(widget->children, child, { DestroyWidget(&child); });
134 }
135 UI_DEL(ppWidget);
136}
137
138uiWidgetPtr uiMakeButtonPtr(uiButton* button) {
139 assert(button);
140 return (uiWidgetPtr){.type = uiTypeButton, .button = button};
141}
142
143uiWidgetPtr uiMakeFramePtr(uiFrame* frame) {
144 assert(frame);
145 return (uiWidgetPtr){.type = uiTypeFrame, .frame = frame};
146}
147
148uiWidgetPtr uiMakeLabelPtr(uiLabel* label) {
149 assert(label);
150 return (uiWidgetPtr){.type = uiTypeLabel, .label = label};
151}
152
153uiWidgetPtr uiMakeTablePtr(uiTable* table) {
154 assert(table);
155 return (uiWidgetPtr){.type = uiTypeTable, .table = table};
156}
157
158void uiWidgetSetParent(uiWidgetPtr child_, uiWidgetPtr parent_) {
159 uiWidget* child = child_.widget;
160 uiWidget* parent = parent_.widget;
161
162 assert(child);
163 assert(parent);
164
165 list_add(parent->children, child);
166}
167
168// -----------------------------------------------------------------------------
169// Button.
170
171uiButton* uiMakeButton(const char* text) {
172 assert(text);
173
174 uiButton* button = UI_NEW(uiButton);
175
176 *button = (uiButton){
177 .widget =
178 (uiWidget){
179 .type = uiTypeButton,
180 .rect = {0},
181 },
182 .text = string_new(text),
183 };
184 return button;
185}
186
187// -----------------------------------------------------------------------------
188// Label.
189
190uiLabel* uiMakeLabel(const char* text) {
191 assert(text);
192
193 uiLabel* label = UI_NEW(uiLabel);
194
195 *label = (uiLabel){
196 .widget =
197 (uiWidget){
198 .type = uiTypeLabel,
199 },
200 .text = string_new(text),
201 };
202 return label;
203}
204
205// -----------------------------------------------------------------------------
206// Frame.
207
208uiFrame* uiMakeFrame(void) {
209 uiFrame* frame = UI_NEW(uiFrame);
210 frame->widget.type = uiTypeFrame;
211 return frame;
212}
213
214void uiDestroyFrame(uiFrame** ppFrame) { DestroyWidget((uiWidget**)ppFrame); }
215
216uiSize uiGetFrameSize(const uiFrame* frame) {
217 assert(frame);
218 return (uiSize){
219 .width = frame->widget.rect.width,
220 .height = frame->widget.rect.height,
221 };
222}
223
224// -----------------------------------------------------------------------------
225// Table.
226
227static const uiCell* GetCell(const uiTable* table, int row, int col) {
228 assert(table);
229 return table->cells + (row * table->cols) + col;
230}
231
232static uiCell* GetCellMut(uiTable* table, int row, int col) {
233 assert(table);
234 return (uiCell*)GetCell(table, row, col);
235}
236
237static uiCell* GetLastRow(uiTable* table) {
238 assert(table);
239 assert(table->rows > 0);
240 return &table->cells[table->cols * (table->rows - 1)];
241}
242
243uiTable* uiMakeTable(int rows, int cols, const char** header) {
244 uiTable* table = UI_NEW(uiTable);
245
246 *table = (uiTable){
247 .widget = (uiWidget){.type = uiTypeTable},
248 .rows = rows,
249 .cols = cols,
250 .widths = (cols > 0) ? calloc(cols, sizeof(int)) : 0,
251 .header = header ? calloc(cols, sizeof(uiCell)) : 0,
252 .cells = (rows * cols > 0) ? calloc(rows * cols, sizeof(uiCell)) : 0,
253 };
254
255 if (header) {
256 for (int col = 0; col < cols; ++col) {
257 table->header[col].child = (uiWidget*)uiMakeLabel(header[col]);
258 }
259 }
260
261 return table;
262}
263
264void uiTableAddRow(uiTable* table, const char** row) {
265 assert(table);
266
267 table->rows++;
268
269 uiCell* cells =
270 realloc(table->cells, table->rows * table->cols * sizeof(uiCell));
271 assert(cells);
272 table->cells = cells;
273
274 uiCell* cell = GetLastRow(table);
275 for (int col = 0; col < table->cols; ++col, ++cell) {
276 cell->child = (uiWidget*)uiMakeLabel(row[col]);
277 }
278}
279
280void uiTableSet(uiTable* table, int row, int col, uiWidgetPtr child) {
281 assert(table);
282 assert(child.widget);
283
284 GetCellMut(table, row, col)->child = child.widget;
285}
286
287const uiWidget* uiTableGet(const uiTable* table, int row, int col) {
288 assert(table);
289 return GetCell(table, row, col)->child;
290}
291
292uiWidget* uiTableGetMut(uiTable* table, int row, int col) {
293 assert(table);
294 return GetCellMut(table, row, col)->child;
295}
296
297// -----------------------------------------------------------------------------
298// Layout and resizing.
299
300static void ResizeTable(uiTable* table, int width, int height) {
301 assert(table);
302
303 if (table->cols == 0) {
304 return;
305 }
306
307 // Surface width: W.
308 // Columns: N
309 //
310 // First, find the minimum width of each column based on their contents.
311 //
312 // If the sum of column widths < N, then distribute the extra space first
313 // among the smallest columns and building up towards the larger.
314 //
315 // If the sum of column widths > N, subtract from the largest column first and
316 // move towards the smaller ones to distribute the space as evenly as
317 // possible.
318
319 // Find the minimum width for each column.
320 int* widths = table->widths;
321 // Header.
322 for (int col = 0; col < table->cols; ++col) {
323 const uiCell* cell = &table->header[col];
324 const uiLabel* label = (uiLabel*)cell->child;
325 const int length = (int)string_length(label->text);
326
327 widths[col] = length;
328 }
329 // Table contents.
330 for (int row = 0; row < table->rows; ++row) {
331 for (int col = 0; col < table->cols; ++col) {
332 const uiCell* cell = GetCell(table, row, col);
333 if (cell->child) {
334 const uiLabel* label = (uiLabel*)cell->child;
335 const int length = (int)string_length(label->text);
336
337 widths[col] = length > widths[col] ? length : widths[col];
338 }
339 }
340 }
341 // Multiply string lengths times glyph width to compute pixel size.
342 for (int col = 0; col < table->cols; ++col) {
343 widths[col] *= g_ui.font->header.glyph_width;
344 }
345
346 // Find the sum of widths.
347 int used_width = 0;
348 for (int col = 0; col < table->cols; ++col) {
349 used_width += widths[col];
350 }
351
352 // Pad if available width is larger than sum of widths.
353 if (used_width < width) {
354 // Divide evenly among columns.
355 // const int extra = width - used_width;
356 // const int pad = extra / table->cols;
357 // const int mod = extra % table->cols;
358 // for (int col = 0; col < table->cols; ++col) {
359 // table->widths[col] += pad + (col < mod ? 1 : 0);
360 // }
361
362 int extra = width - used_width;
363 while (extra > 0) {
364 // Find smallest column.
365 int smallest = 0;
366 for (int col = 1; col < table->cols; ++col) {
367 if (widths[col] < widths[smallest]) {
368 smallest = col;
369 }
370 }
371 // Pad it and subtract from the budget.
372 widths[smallest] += 1;
373 extra--;
374 }
375 }
376 // Shrink if available width is smaller than the sum of widths.
377 else if (used_width > width) {
378 int deficit = used_width - width;
379 while (deficit > 0) {
380 // Find largest column.
381 int largest = 0;
382 for (int col = 1; col < table->cols; ++col) {
383 if (widths[col] > widths[largest]) {
384 largest = col;
385 }
386 }
387 // Shrink it and subtract from the deficit.
388 widths[largest] -= 1;
389 deficit--;
390 }
391 }
392}
393
394static void ResizeWidget(uiWidget* widget, int width, int height) {
395 assert(widget);
396
397 widget->rect.width = width;
398 widget->rect.height = height;
399
400 switch (widget->type) {
401 case uiTypeButton:
402 break;
403 case uiTypeFrame:
404 list_foreach_mut(
405 widget->children, child, { ResizeWidget(child, width, height); });
406 break;
407 case uiTypeLabel:
408 break;
409 case uiTypeTable:
410 ResizeTable((uiTable*)widget, width, height);
411 break;
412 case uiTypeMax:
413 TRAP();
414 break;
415 }
416}
417
418void uiResizeFrame(uiFrame* frame, int width, int height) {
419 assert(frame);
420 ResizeWidget(&frame->widget, width, height);
421}
422
423// -----------------------------------------------------------------------------
424// Rendering.
425
426static const uiPixel uiBlack = {40, 40, 40, 255};
427static const uiPixel uiWhite = {255, 255, 255, 255};
428static const uiPixel uiPink = {128, 0, 128, 255};
429
430/// Render state.
431///
432/// Render functions are allowed to manipulate the state internally (e.g., the
433/// subsurface), but must leave the state intact before returning, except, of
434/// course, for the rendered pixels.
435///
436/// We store a subsurface separate from the surface so that we can always check
437/// whether a given coordinate is within the bounds of the physical surface.
438typedef struct RenderState {
439 uiSurface surface; /// Surface of pixels on which the UI is rendered.
440 uiRect subsurface; /// Subregion where the current UI widget is rendered.
441 uiPoint pen; /// Current pen position relative to subsurface.
442} RenderState;
443
444static void RenderWidget(RenderState* state, const uiWidget* widget);
445
446void PushSubsurface(
447 RenderState* state, int width, int height, uiRect* original_subsurface,
448 uiPoint* original_pen) {
449 assert(state);
450 assert(original_subsurface);
451 assert(original_pen);
452
453 *original_subsurface = state->subsurface;
454 *original_pen = state->pen;
455
456 state->subsurface.x = state->subsurface.x + state->pen.x;
457 state->subsurface.width = width;
458 state->subsurface.height = height;
459 state->pen.x = 0;
460}
461
462void PopSubsurface(
463 RenderState* state, const uiRect* original_subsurface,
464 const uiPoint* original_pen) {
465 assert(state);
466 assert(original_subsurface);
467 assert(original_pen);
468
469 state->subsurface = *original_subsurface;
470 state->pen = *original_pen;
471}
472
473/// Checks whether pen + (w,h) is within the surface and subsurface.
474static bool PenInSurface(const RenderState* state, int w, int h) {
475 assert(state);
476
477 // Surface.
478 const bool in_surface =
479 ((state->subsurface.x + state->pen.x + w) < state->surface.width) &&
480 ((state->subsurface.y + state->pen.y + h) < state->surface.height);
481
482 // Subsurface.
483 const bool in_subsurface = ((state->pen.x + w) < state->subsurface.width) &&
484 ((state->pen.y + h) < state->subsurface.height);
485
486 return in_surface && in_subsurface;
487}
488
489/// Get the pixel at (x,y).
490static uiPixel* SurfaceXy(uiSurface* surface, int x, int y) {
491 assert(surface);
492 assert(x >= 0);
493 assert(y >= 0);
494 assert(x < surface->width);
495 assert(y < surface->height);
496 return surface->pixels + (surface->width * y) + x;
497}
498
499/// Get the pixel at pen + (x,y).
500static uiPixel* PixelXy(RenderState* state, int x, int y) {
501 assert(state);
502 return SurfaceXy(
503 &state->surface, state->subsurface.x + state->pen.x + x,
504 state->subsurface.y + state->pen.y + y);
505}
506
507static void FillRect(const uiRect* rect, uiPixel colour, RenderState* state) {
508 assert(rect);
509 assert(state);
510 assert(rect->width <= state->subsurface.width);
511 assert(rect->height <= state->subsurface.height);
512
513 for (int y = rect->y; y < rect->y + rect->height; ++y) {
514 uiPixel* pixel = PixelXy(state, rect->x, y);
515 for (int x = rect->x; x < rect->x + rect->width; ++x) {
516 *pixel++ = colour;
517 }
518 }
519}
520
521/// Render a glyph.
522/// The glyph is clamped to the surface's bounds.
523static void RenderGlyph(
524 const FontAtlas* atlas, unsigned char c, RenderState* state) {
525 assert(atlas);
526 assert(state);
527 assert(atlas->header.glyph_width <= state->subsurface.width);
528 assert(atlas->header.glyph_height <= state->subsurface.height);
529
530 const int glyph_width = atlas->header.glyph_width;
531 const int glyph_height = atlas->header.glyph_height;
532
533 const unsigned char* glyph = FontGetGlyph(atlas, c);
534
535 for (int y = 0; (y < atlas->header.glyph_height) &&
536 PenInSurface(state, glyph_width - 1, glyph_height - 1);
537 ++y) {
538 for (int x = 0; (x < atlas->header.glyph_width) &&
539 PenInSurface(state, glyph_width - 1, glyph_height - 1);
540 ++x, ++glyph) {
541 uiPixel* pixel = PixelXy(state, x, y);
542 if (*glyph > 0) {
543 pixel->r = *glyph;
544 pixel->g = *glyph;
545 pixel->b = *glyph;
546 pixel->a = 255;
547 }
548 }
549 }
550}
551
552static void RenderText(const char* text, size_t length, RenderState* state) {
553 assert(text);
554 assert(state);
555
556 const FontAtlas* atlas = g_ui.font;
557
558 const int glyph_width = atlas->header.glyph_width;
559 const int glyph_height = atlas->header.glyph_height;
560
561 // Save the x-pen so that we can restore it after rendering the text.
562 const int x0 = state->pen.x;
563
564 // Truncate the text rendering if it exceeds the subsurface's width or height.
565 const char* c = text;
566 for (size_t i = 0;
567 (i < length) && PenInSurface(state, glyph_width - 1, glyph_height - 1);
568 ++i, ++c, state->pen.x += glyph_width) {
569 RenderGlyph(atlas, *c, state);
570 }
571
572 state->pen.x = x0;
573}
574
575static void RenderFrame(const uiFrame* frame, RenderState* state) {
576 assert(frame);
577
578 FillRect(&frame->widget.rect, uiBlack, state);
579}
580
581static void RenderLabel(const uiLabel* label, RenderState* state) {
582 assert(label);
583 assert(state);
584
585 RenderText(string_data(label->text), string_length(label->text), state);
586}
587
588static void RenderTable(const uiTable* table, RenderState* state) {
589 assert(table);
590 assert(state);
591
592 const int x0 = state->pen.x;
593 const int y0 = state->pen.y;
594
595 uiRect original_subsurface = {0};
596 uiPoint original_pen = {0};
597
598 // Render header.
599 if (table->header) {
600 for (int col = 0; col < table->cols; ++col) {
601 // Crop the column contents to the column width so that one column does
602 // not spill into the next.
603 PushSubsurface(
604 state, table->widths[col], state->subsurface.height,
605 &original_subsurface, &original_pen);
606
607 const uiCell* cell = &table->header[col];
608 RenderWidget(state, cell->child);
609
610 // Reset the original subsurface and pen for subsequent columns.
611 PopSubsurface(state, &original_subsurface, &original_pen);
612
613 // Next column.
614 state->pen.x += table->widths[col];
615 }
616 }
617 state->pen.x = x0;
618 state->pen.y += g_ui.font->header.glyph_height;
619
620 // Render rows.
621 for (int row = 0; (row < table->rows) && PenInSurface(state, 0, 0); ++row) {
622 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
624 // not spill into the next.
625 PushSubsurface(
626 state, table->widths[col], state->subsurface.height,
627 &original_subsurface, &original_pen);
628
629 state->subsurface.x = state->subsurface.x + state->pen.x;
630 state->subsurface.width = table->widths[col];
631 state->pen.x = 0;
632
633 const uiCell* cell = GetCell(table, row, col);
634 RenderWidget(state, cell->child);
635
636 // Reset the original subsurface and pen for subsequent columns.
637 PopSubsurface(state, &original_subsurface, &original_pen);
638
639 // Next column.
640 state->pen.x += table->widths[col];
641 }
642 state->pen.x = x0;
643 state->pen.y += g_ui.font->header.glyph_height;
644 }
645 state->pen.y = y0;
646}
647
648static void RenderWidget(RenderState* state, const uiWidget* widget) {
649 assert(state);
650 assert(widget);
651
652 // Render this widget.
653 switch (widget->type) {
654 case uiTypeButton:
655 break;
656 case uiTypeFrame:
657 RenderFrame((const uiFrame*)widget, state);
658 break;
659 case uiTypeLabel:
660 RenderLabel((const uiLabel*)widget, state);
661 break;
662 case uiTypeTable:
663 RenderTable((const uiTable*)widget, state);
664 break;
665 case uiTypeMax:
666 TRAP();
667 break;
668 }
669
670 // Render children.
671 list_foreach(widget->children, child, { RenderWidget(state, child); });
672}
673
674void uiRender(const uiFrame* frame, uiSurface* surface) {
675 assert(frame);
676 assert(surface);
677
678 RenderWidget(
679 &(RenderState){
680 .surface = *surface,
681 .subsurface =
682 (uiRect){
683 .x = 0,
684 .y = 0,
685 .width = surface->width,
686 .height = surface->height},
687 .pen = {.x = 0, .y = 0},
688 },
689 (const uiWidget*)frame);
690}