summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2026-03-12 15:29:23 -0700
committer3gg <3gg@shellblade.net>2026-03-12 15:29:23 -0700
commit92978a10576d52a0f6c9983d3b6afae7c40eff40 (patch)
treebf73faed8aa1ecd71b9f61c37a549faf4cd30372
parent58c0f40df5947b3933bf7b6564b2ba5dc39fbd92 (diff)
Support scrolling by dragging scrollbars
-rw-r--r--CMakeLists.txt1
-rw-r--r--include/ui.h15
-rw-r--r--src/constants.h2
-rw-r--r--src/input.c226
-rw-r--r--src/layout.c21
-rw-r--r--src/render.c13
-rw-r--r--src/uiLibrary.h23
-rw-r--r--src/widget/scrollbar.c8
-rw-r--r--src/widget/table.c58
-rw-r--r--src/widget/widget.c21
-rw-r--r--src/widget/widget.h49
11 files changed, 355 insertions, 82 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 6279635..043bb2d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -20,6 +20,7 @@ add_library(ui
20 src/widget/button.c 20 src/widget/button.c
21 src/widget/frame.c 21 src/widget/frame.c
22 src/widget/label.c 22 src/widget/label.c
23 src/widget/scrollbar.c
23 src/widget/table.c 24 src/widget/table.c
24 src/widget/widget.c 25 src/widget/widget.c
25 src/widget/widget.h) 26 src/widget/widget.h)
diff --git a/include/ui.h b/include/ui.h
index b95157e..baaa550 100644
--- a/include/ui.h
+++ b/include/ui.h
@@ -82,7 +82,7 @@ typedef enum uiMouseButtonState {
82/// Mouse button event. 82/// Mouse button event.
83typedef struct uiMouseButtonEvent { 83typedef struct uiMouseButtonEvent {
84 uiMouseButton button; 84 uiMouseButton button;
85 uiMouseButtonState state; 85 uiMouseButtonState button_state;
86 uiPoint mouse_position; 86 uiPoint mouse_position;
87} uiMouseButtonEvent; 87} uiMouseButtonEvent;
88 88
@@ -98,11 +98,19 @@ typedef struct uiMouseScrollEvent {
98 int scroll_offset; /// Positive = down; negative = up. 98 int scroll_offset; /// Positive = down; negative = up.
99} uiMouseScrollEvent; 99} uiMouseScrollEvent;
100 100
101/// Mouse move event.
102typedef struct uiMouseMoveEvent {
103 // TODO: A bitfield would be sufficient.
104 uiMouseButtonState button_state[uiMouseButtonMax];
105 uiPoint mouse_position;
106} uiMouseMoveEvent;
107
101/// Input event type. 108/// Input event type.
102typedef enum uiInputEventType { 109typedef enum uiInputEventType {
103 uiEventMouseButton, 110 uiEventMouseButton,
104 uiEventMouseClick, 111 uiEventMouseClick,
105 uiEventMouseScroll, 112 uiEventMouseScroll,
113 uiEventMouseMove,
106} uiInputEventType; 114} uiInputEventType;
107 115
108/// Input event. 116/// Input event.
@@ -112,6 +120,7 @@ typedef struct uiInputEvent {
112 uiMouseButtonEvent mouse_button; 120 uiMouseButtonEvent mouse_button;
113 uiMouseClickEvent mouse_click; 121 uiMouseClickEvent mouse_click;
114 uiMouseScrollEvent mouse_scroll; 122 uiMouseScrollEvent mouse_scroll;
123 uiMouseMoveEvent mouse_move;
115 }; 124 };
116} uiInputEvent; 125} uiInputEvent;
117 126
@@ -149,6 +158,9 @@ uiPtr uiMakeButtonPtr(uiButton*);
149uiPtr uiMakeFramePtr(uiFrame*); 158uiPtr uiMakeFramePtr(uiFrame*);
150uiPtr uiMakeLabelPtr(uiLabel*); 159uiPtr uiMakeLabelPtr(uiLabel*);
151uiPtr uiMakeTablePtr(uiTable*); 160uiPtr uiMakeTablePtr(uiTable*);
161uiPtr uiMakeWidgetPtr(uiWidget*);
162uiPtr uiNullptr(void);
163bool uiIsNullptr(uiPtr ptr);
152 164
153uiButton* uiGetButtonPtr(uiPtr ptr); 165uiButton* uiGetButtonPtr(uiPtr ptr);
154uiFrame* uiGetFramePtr(uiPtr ptr); 166uiFrame* uiGetFramePtr(uiPtr ptr);
@@ -188,6 +200,7 @@ void uiTableClear(uiTable*);
188void uiTableAddRow(uiTable*, const char** row); 200void uiTableAddRow(uiTable*, const char** row);
189void uiTableSet(uiTable*, int row, int col, const char* text); 201void uiTableSet(uiTable*, int row, int col, const char* text);
190const char* uiTableGet(const uiTable*, int row, int col); 202const char* uiTableGet(const uiTable*, int row, int col);
203void uiTableScroll(uiTable*, int row);
191 204
192// ----------------------------------------------------------------------------- 205// -----------------------------------------------------------------------------
193// Rendering. 206// Rendering.
diff --git a/src/constants.h b/src/constants.h
index 0d93d14..47babab 100644
--- a/src/constants.h
+++ b/src/constants.h
@@ -4,4 +4,4 @@
4#define MaxWidgetEvents 8 4#define MaxWidgetEvents 8
5 5
6// Width of scroll bars in pixels. 6// Width of scroll bars in pixels.
7#define ScrollBarWidth 32 7#define ScrollbarWidth 32
diff --git a/src/input.c b/src/input.c
index 4461c8b..6a7f4d0 100644
--- a/src/input.c
+++ b/src/input.c
@@ -6,9 +6,6 @@
6 6
7#include <cassert.h> 7#include <cassert.h>
8 8
9#define Min(a, b) ((a) < (b) ? (a) : (b))
10#define Max(a, b) ((a) > (b) ? (a) : (b))
11
12/// Return true if the rectangle contains the point. 9/// Return true if the rectangle contains the point.
13static bool RectContains(uiRect rect, uiPoint point) { 10static bool RectContains(uiRect rect, uiPoint point) {
14 return (rect.x <= point.x) && (point.x <= (rect.x + rect.width)) && 11 return (rect.x <= point.x) && (point.x <= (rect.x + rect.width)) &&
@@ -35,17 +32,59 @@ static uiWidget* GetWidgetUnderMouse(uiWidget* parent, uiPoint mouse) {
35 return 0; 32 return 0;
36} 33}
37 34
38/// Get the table row at the given pixel position. 35// -----------------------------------------------------------------------------
36// Scrollbar.
37
38/// Process a scrollbar mouse button event.
39static void MouseButtonScrollbar(
40 uiScrollbar* scrollbar, const uiMouseButtonEvent* event) {
41 assert(scrollbar);
42 assert(event);
43
44 if (event->button_state == uiMouseDown) {
45 UI_LOG("scroll start");
46 // TODO: I don't quite like this global state, but I also don't want to
47 // store input state inside the widgets. Define a struct for input handling
48 // and pass it to these functions instead?
49 g_ui.scroll.scrollbar_handle_y_start = scrollbar->handle_y;
50 g_ui.scroll.scrolling = true;
51 } else if (event->button_state == uiMouseUp) {
52 UI_LOG("scroll end");
53 g_ui.scroll.scrolling = false;
54 }
55}
56
57/// Process a scrollbar mouse move event.
58///
59/// Return the amount scrolled relative to the scroll starting position, in
60/// pixels.
61static int MouseMoveScrollbar(
62 uiScrollbar* scrollbar, const uiMouseMoveEvent* event) {
63 assert(scrollbar);
64 assert(event);
65
66 const int delta = event->mouse_position.y - g_ui.mouse_down.start_point.y;
67 ScrollbarScroll(scrollbar, g_ui.scroll.scrollbar_handle_y_start + delta);
68 return delta;
69}
70
71// -----------------------------------------------------------------------------
72// Table.
73
74/// Get the table row and column at the given pixel position, or whether the
75/// scrollbar was hit.
39static void GetTableRowColAtXy( 76static void GetTableRowColAtXy(
40 const uiTable* table, uiPoint p, int* out_row, int* out_col) { 77 const uiTable* table, uiPoint p, int* out_row, int* out_col,
78 bool* out_scrollbar) {
41 assert(table); 79 assert(table);
42 assert(out_row); 80 assert(out_row);
43 assert(out_col); 81 assert(out_col);
44 82
45 const uiWidget* widget = (uiWidget*)table; 83 const uiWidget* widget = (uiWidget*)table;
46 84
47 int col = -1; 85 int col = -1;
48 int row = -1; 86 int row = -1;
87 bool scrollbar = false;
49 88
50 if (RectContains(widget->rect, p)) { 89 if (RectContains(widget->rect, p)) {
51 int x = p.x - widget->rect.x; 90 int x = p.x - widget->rect.x;
@@ -55,14 +94,40 @@ static void GetTableRowColAtXy(
55 // 0 is the header, and we want to map the first row to 0, so -1. 94 // 0 is the header, and we want to map the first row to 0, so -1.
56 row = table->offset + 95 row = table->offset +
57 ((p.y - widget->rect.y) / g_ui.font->header.glyph_height) - 1; 96 ((p.y - widget->rect.y) / g_ui.font->header.glyph_height) - 1;
58 // Out-of-bounds check. 97 // Scrollbar area check.
59 if ((col >= table->cols) || (row >= table->rows)) { 98 if ((col >= table->cols) && (row < table->rows)) {
99 col = -1;
100 scrollbar = true;
101 }
102 // Out of bounds.
103 else if ((col >= table->cols) || (row >= table->rows)) {
60 col = row = -1; 104 col = row = -1;
61 } 105 }
62 } 106 }
63 107
64 *out_col = col; 108 *out_col = col;
65 *out_row = row; 109 *out_row = row;
110 *out_scrollbar = scrollbar;
111}
112
113/// Process a table mouse button event.
114static void MouseButtonTable(uiTable* table, const uiMouseButtonEvent* event) {
115 assert(table);
116 assert(event);
117
118 if (event->button_state == uiMouseDown) {
119 int row, col;
120 bool scrollbar;
121 GetTableRowColAtXy(table, event->mouse_position, &row, &col, &scrollbar);
122
123 if (scrollbar) {
124 MouseButtonScrollbar(&table->scrollbar, event);
125 }
126 } else if ((event->button_state == uiMouseUp) && g_ui.scroll.scrolling) {
127 // The mouse up event need not happen while the mouse is on the scrollbar,
128 // so process mouse-up regardless of whether the mouse is.
129 MouseButtonScrollbar(&table->scrollbar, event);
130 }
66} 131}
67 132
68/// Process a table click event. 133/// Process a table click event.
@@ -70,8 +135,9 @@ static void ClickTable(uiTable* table, const uiMouseClickEvent* event) {
70 assert(table); 135 assert(table);
71 assert(event); 136 assert(event);
72 137
73 int row, col; 138 int row, col;
74 GetTableRowColAtXy(table, event->mouse_position, &row, &col); 139 bool scrollbar;
140 GetTableRowColAtXy(table, event->mouse_position, &row, &col, &scrollbar);
75 141
76 if ((row != -1) && (col != -1)) { 142 if ((row != -1) && (col != -1)) {
77 PushWidgetEvent(&(uiWidgetEvent){ 143 PushWidgetEvent(&(uiWidgetEvent){
@@ -86,14 +152,25 @@ static void ClickTable(uiTable* table, const uiMouseClickEvent* event) {
86static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) { 152static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) {
87 assert(table); 153 assert(table);
88 assert(event); 154 assert(event);
89 table->offset = 155
90 Min(table->rows - table->num_visible_rows, 156 const int row = table->offset - event->scroll_offset;
91 Max(0, table->offset - event->scroll_offset)); 157 uiTableScroll(table, row);
92} 158}
93 159
94/// Process a scroll event. 160/// Process a table mouse move event.
95static bool ProcessScrollEvent( 161static void MouseMoveTable(uiTable* table, const uiMouseMoveEvent* event) {
96 uiWidget* widget, const uiMouseScrollEvent* event) { 162 assert(table);
163 assert(event);
164
165 if (g_ui.scroll.scrolling) {
166 MouseMoveScrollbar(&table->scrollbar, event);
167 SyncTableToScrollbar(table);
168 }
169}
170
171/// Process a mouse button event.
172static bool ProcessMouseButtonEvent(
173 uiWidget* widget, const uiMouseButtonEvent* event) {
97 assert(widget); 174 assert(widget);
98 assert(event); 175 assert(event);
99 176
@@ -101,7 +178,7 @@ static bool ProcessScrollEvent(
101 178
102 switch (widget->type) { 179 switch (widget->type) {
103 case uiTypeTable: 180 case uiTypeTable:
104 ScrollTable((uiTable*)widget, event); 181 MouseButtonTable((uiTable*)widget, event);
105 processed = true; 182 processed = true;
106 break; 183 break;
107 default: 184 default:
@@ -112,7 +189,7 @@ static bool ProcessScrollEvent(
112} 189}
113 190
114/// Process a click event. 191/// Process a click event.
115static bool ProcessClickEvent( 192static bool ProcessMouseClickEvent(
116 uiWidget* widget, const uiMouseClickEvent* event) { 193 uiWidget* widget, const uiMouseClickEvent* event) {
117 assert(widget); 194 assert(widget);
118 assert(event); 195 assert(event);
@@ -131,21 +208,89 @@ static bool ProcessClickEvent(
131 return processed; 208 return processed;
132} 209}
133 210
211/// Process a scroll event.
212static bool ProcessMouseScrollEvent(
213 uiWidget* widget, const uiMouseScrollEvent* event) {
214 assert(widget);
215 assert(event);
216
217 bool processed = false;
218
219 switch (widget->type) {
220 case uiTypeTable:
221 ScrollTable((uiTable*)widget, event);
222 processed = true;
223 break;
224 default:
225 break;
226 }
227
228 return processed;
229}
230
231/// Process a mouse move event.
232static bool ProcessMouseMoveEvent(
233 uiWidget* widget, const uiMouseMoveEvent* event) {
234 assert(widget);
235 assert(event);
236
237 bool processed = false;
238
239 switch (widget->type) {
240 case uiTypeTable:
241 MouseMoveTable((uiTable*)widget, event);
242 processed = true;
243 break;
244 default:
245 break;
246 }
247
248 return processed;
249}
250
134bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) { 251bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) {
135 assert(frame); 252 assert(frame);
136 assert(event); 253 assert(event);
137 254
138 uiWidget* widget = (uiWidget*)frame; 255 // TODO: processed != redraw. The client will redraw if this function
139 256 // returns
257 // true, but whether an event was processed does it imply that the UI needs
258 // a redraw.
259 //
260 // TODO: Also think about limiting redraws in xplorer to like 25fps or
261 // something in a "battery saving" mode of sorts.
140 bool processed = false; 262 bool processed = false;
141 263
142 switch (event->type) { 264 switch (event->type) {
143 case uiEventMouseButton: { 265 case uiEventMouseButton: {
144 const uiMouseButtonEvent* ev = &event->mouse_button; 266 const uiMouseButtonEvent* ev = &event->mouse_button;
145 267
146 uiMouseButtonState* prev_state = &g_ui.mouse_button_state[ev->button]; 268 // Update the mouse button state.
269 uiMouseButtonState* button_state = &g_ui.mouse_button_state[ev->button];
270 const uiMouseButtonState prev_state = *button_state;
271 *button_state = ev->button_state;
272
273 // If this is a mouse up event and a widget is currently being
274 // mouse-downed, send the event to that widget.
275 if ((ev->button_state == uiMouseUp) &&
276 !uiIsNullptr(g_ui.mouse_down.widget)) {
277 processed = ProcessMouseButtonEvent(g_ui.mouse_down.widget.widget, ev);
278 g_ui.mouse_down = (uiMouseDownState){0};
279 } else { // Mouse down or no widget.
280 uiWidget* target =
281 GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position);
282 if (target) {
283 processed = ProcessMouseButtonEvent(target, ev);
284 if (processed && (ev->button_state == uiMouseDown)) {
285 g_ui.mouse_down.widget = uiMakeWidgetPtr(target);
286 g_ui.mouse_down.start_point = ev->mouse_position;
287 } else {
288 g_ui.mouse_down = (uiMouseDownState){0};
289 }
290 }
291 }
147 292
148 if ((*prev_state == uiMouseDown) && (ev->state == uiMouseUp)) { 293 if ((prev_state == uiMouseDown) && (ev->button_state == uiMouseUp)) {
149 // Click. 294 // Click.
150 uiSendEvent( 295 uiSendEvent(
151 frame, 296 frame,
@@ -155,26 +300,45 @@ bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) {
155 .button = ev->button, .mouse_position = ev->mouse_position} 300 .button = ev->button, .mouse_position = ev->mouse_position}
156 }); 301 });
157 } 302 }
158
159 *prev_state = ev->state;
160 break; 303 break;
161 } 304 }
162 case uiEventMouseClick: { 305 case uiEventMouseClick: {
163 const uiMouseClickEvent* ev = &event->mouse_click; 306 const uiMouseClickEvent* ev = &event->mouse_click;
164 uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position); 307
308 uiWidget* target =
309 GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position);
165 if (target) { 310 if (target) {
166 processed = ProcessClickEvent(target, ev); 311 processed = ProcessMouseClickEvent(target, ev);
167 } 312 }
168 break; 313 break;
169 } 314 }
170 case uiEventMouseScroll: { 315 case uiEventMouseScroll: {
171 const uiMouseScrollEvent* ev = &event->mouse_scroll; 316 const uiMouseScrollEvent* ev = &event->mouse_scroll;
172 uiWidget* target = GetWidgetUnderMouse(widget, ev->mouse_position); 317
318 uiWidget* target =
319 GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position);
173 if (target) { 320 if (target) {
174 processed = ProcessScrollEvent(target, ev); 321 processed = ProcessMouseScrollEvent(target, ev);
175 } 322 }
176 break; 323 break;
177 } 324 }
325 case uiEventMouseMove: {
326 const uiMouseMoveEvent* ev = &event->mouse_move;
327
328 // If a widget is currently being moused-down, send the event to that
329 // widget.
330 if (!uiIsNullptr(g_ui.mouse_down.widget)) {
331 processed = ProcessMouseMoveEvent(g_ui.mouse_down.widget.widget, ev);
332 } else {
333 uiWidget* target =
334 GetWidgetUnderMouse((uiWidget*)frame, ev->mouse_position);
335 if (target) {
336 processed = ProcessMouseMoveEvent(target, ev);
337 }
338 }
339
340 break;
341 }
178 } 342 }
179 343
180 return processed; 344 return processed;
diff --git a/src/layout.c b/src/layout.c
index c8f5995..5261eb3 100644
--- a/src/layout.c
+++ b/src/layout.c
@@ -51,7 +51,7 @@ static void ResizeTable(uiTable* table, int width, int height) {
51 // Table contents. 51 // Table contents.
52 for (int row = 0; row < table->rows; ++row) { 52 for (int row = 0; row < table->rows; ++row) {
53 for (int col = 0; col < table->cols; ++col) { 53 for (int col = 0; col < table->cols; ++col) {
54 const uiCell* cell = GetCell(table, row, col); 54 const uiCell* cell = TableGetCell(table, row, col);
55 const int length = (int)string_length(cell->text); 55 const int length = (int)string_length(cell->text);
56 56
57 widths[col] = length > widths[col] ? length : widths[col]; 57 widths[col] = length > widths[col] ? length : widths[col];
@@ -110,13 +110,28 @@ static void ResizeTable(uiTable* table, int width, int height) {
110 } 110 }
111 111
112 // Now make room for the scroll bar, if necessary. 112 // Now make room for the scroll bar, if necessary.
113 uiScrollbar* scrollbar = &table->scrollbar;
113 if (table->flags.vertical_overflow) { 114 if (table->flags.vertical_overflow) {
114 const int offset = ScrollBarWidth / table->cols; 115 // Subtract room from table columns.
115 const int remainder = ScrollBarWidth % table->cols; 116 const int offset = ScrollbarWidth / table->cols;
117 const int remainder = ScrollbarWidth % table->cols;
116 for (int col = 0; col < table->cols; ++col) { 118 for (int col = 0; col < table->cols; ++col) {
117 table->widths[col] -= offset + (col < remainder ? 1 : 0); 119 table->widths[col] -= offset + (col < remainder ? 1 : 0);
118 assert(table->widths[col] >= 0); 120 assert(table->widths[col] >= 0);
119 } 121 }
122
123 // Set scrollbar layout.
124 scrollbar->width = ScrollbarWidth;
125 scrollbar->height = table->height;
126 scrollbar->handle_height =
127 (int)((double)table->num_visible_rows / (double)table->rows *
128 (double)table->height);
129 uiTableScroll(table, table->offset);
130 } else { // Scroll bar not visible.
131 scrollbar->width = 0;
132 scrollbar->height = 0;
133 scrollbar->handle_height = 0;
134 scrollbar->handle_y = 0;
120 } 135 }
121} 136}
122 137
diff --git a/src/render.c b/src/render.c
index 20ab142..a242504 100644
--- a/src/render.c
+++ b/src/render.c
@@ -224,7 +224,7 @@ static void RenderTable(const uiTable* table, RenderState* state) {
224 state->subsurface.width = table->widths[col]; 224 state->subsurface.width = table->widths[col];
225 state->pen.x = 0; 225 state->pen.x = 0;
226 226
227 const uiCell* cell = GetCell(table, row, col); 227 const uiCell* cell = TableGetCell(table, row, col);
228 RenderText(string_data(cell->text), string_length(cell->text), state); 228 RenderText(string_data(cell->text), string_length(cell->text), state);
229 229
230 // Reset the original subsurface and pen for subsequent columns. 230 // Reset the original subsurface and pen for subsequent columns.
@@ -242,14 +242,11 @@ static void RenderTable(const uiTable* table, RenderState* state) {
242 if (table->flags.vertical_overflow) { 242 if (table->flags.vertical_overflow) {
243 state->pen.x = col_widths_sum; 243 state->pen.x = col_widths_sum;
244 244
245 const int y_start = (int)((double)table->offset / (double)table->rows * 245 const uiScrollbar* scrollbar = &table->scrollbar;
246 (double)table->height);
247
248 const int height = (int)((double)table->num_visible_rows /
249 (double)table->rows * (double)table->height);
250
251 FillRect( 246 FillRect(
252 &(uiRect){.y = y_start, .width = ScrollBarWidth, .height = height}, 247 &(uiRect){.y = scrollbar->handle_y,
248 .width = ScrollbarWidth,
249 .height = scrollbar->handle_height},
253 uiPink, state); 250 uiPink, state);
254 251
255 state->pen.x = x0; 252 state->pen.x = x0;
diff --git a/src/uiLibrary.h b/src/uiLibrary.h
index 98719d7..f6faf01 100644
--- a/src/uiLibrary.h
+++ b/src/uiLibrary.h
@@ -6,8 +6,31 @@
6 6
7#include <font.h> 7#include <font.h>
8 8
9#ifndef NDEBUG
10#include <stdio.h>
11#define UI_LOG(...) printf("[ui] " __VA_ARGS__ "\n")
12#else
13#define UI_LOG
14#endif
15
16/// Input handling of scrollbars.
17typedef struct uiScrollState {
18 int scrollbar_handle_y_start; // y-coordinate of the scrollbar handle at the
19 // start of the scroll.
20 int table_offset_start; // Row offset of a table at the start of the scroll.
21 bool scrolling; // Whether a scrollbar is being dragged.
22} uiScrollState;
23
24/// Input handling of mouse-down events.
25typedef struct uiMouseDownState {
26 uiPtr widget; // The widget currently processing a mouse down event.
27 uiPoint start_point; // Mouse position when the mouse went down.
28} uiMouseDownState;
29
9typedef struct uiLibrary { 30typedef struct uiLibrary {
10 FontAtlas* font; 31 FontAtlas* font;
32 uiScrollState scroll;
33 uiMouseDownState mouse_down;
11 uiMouseButtonState mouse_button_state[uiMouseButtonMax]; 34 uiMouseButtonState mouse_button_state[uiMouseButtonMax];
12 uiWidgetEvent widget_events[MaxWidgetEvents]; 35 uiWidgetEvent widget_events[MaxWidgetEvents];
13 int num_widget_events; 36 int num_widget_events;
diff --git a/src/widget/scrollbar.c b/src/widget/scrollbar.c
new file mode 100644
index 0000000..9cece5d
--- /dev/null
+++ b/src/widget/scrollbar.c
@@ -0,0 +1,8 @@
1#include "widget.h"
2
3int ScrollbarScroll(uiScrollbar* scrollbar, int y) {
4 assert(scrollbar);
5 scrollbar->handle_y =
6 Max(0, Min(scrollbar->height - scrollbar->handle_height, y));
7 return scrollbar->handle_y;
8}
diff --git a/src/widget/table.c b/src/widget/table.c
index 76a0413..e7d412e 100644
--- a/src/widget/table.c
+++ b/src/widget/table.c
@@ -1,20 +1,7 @@
1#include "widget.h" 1#include "widget.h"
2 2
3const uiCell* GetCell(const uiTable* table, int row, int col) { 3#define Min(a, b) ((a) < (b) ? (a) : (b))
4 assert(table); 4#define Max(a, b) ((a) > (b) ? (a) : (b))
5 return &table->cells[row][col];
6}
7
8uiCell* GetCellMut(uiTable* table, int row, int col) {
9 assert(table);
10 return (uiCell*)GetCell(table, row, col);
11}
12
13uiCell** GetLastRow(uiTable* table) {
14 assert(table);
15 assert(table->rows > 0);
16 return &table->cells[table->rows - 1];
17}
18 5
19uiTable* uiMakeTable(int rows, int cols, const char** header) { 6uiTable* uiMakeTable(int rows, int cols, const char** header) {
20 uiTable* table = UI_NEW(uiTable); 7 uiTable* table = UI_NEW(uiTable);
@@ -73,7 +60,7 @@ void uiTableAddRow(uiTable* table, const char** row) {
73 ASSERT(cells); 60 ASSERT(cells);
74 table->cells = cells; 61 table->cells = cells;
75 62
76 uiCell** pLastRow = GetLastRow(table); 63 uiCell** pLastRow = TableGetLastRow(table);
77 *pLastRow = calloc(table->cols, sizeof(uiCell)); 64 *pLastRow = calloc(table->cols, sizeof(uiCell));
78 ASSERT(*pLastRow); 65 ASSERT(*pLastRow);
79 uiCell* lastRow = *pLastRow; 66 uiCell* lastRow = *pLastRow;
@@ -86,10 +73,45 @@ void uiTableAddRow(uiTable* table, const char** row) {
86void uiTableSet(uiTable* table, int row, int col, const char* text) { 73void uiTableSet(uiTable* table, int row, int col, const char* text) {
87 assert(table); 74 assert(table);
88 assert(text); 75 assert(text);
89 GetCellMut(table, row, col)->text = string_new(text); 76 TableGetCellMut(table, row, col)->text = string_new(text);
90} 77}
91 78
92const char* uiTableGet(const uiTable* table, int row, int col) { 79const char* uiTableGet(const uiTable* table, int row, int col) {
93 assert(table); 80 assert(table);
94 return string_data(GetCell(table, row, col)->text); 81 return string_data(TableGetCell(table, row, col)->text);
82}
83
84void uiTableScroll(uiTable* table, int row) {
85 assert(table);
86 table->offset = Min(table->rows - table->num_visible_rows, Max(0, row));
87 SyncScrollbarToTable(table);
88}
89
90void SyncScrollbarToTable(uiTable* table) {
91 assert(table);
92 ScrollbarScroll(
93 &table->scrollbar, (int)((double)table->offset / (double)table->rows *
94 (double)table->height));
95}
96
97void SyncTableToScrollbar(uiTable* table) {
98 assert(table);
99 table->offset = (int)((double)table->scrollbar.handle_y /
100 (double)table->height * (double)table->rows);
101}
102
103const uiCell* TableGetCell(const uiTable* table, int row, int col) {
104 assert(table);
105 return &table->cells[row][col];
106}
107
108uiCell* TableGetCellMut(uiTable* table, int row, int col) {
109 assert(table);
110 return (uiCell*)TableGetCell(table, row, col);
111}
112
113uiCell** TableGetLastRow(uiTable* table) {
114 assert(table);
115 assert(table->rows > 0);
116 return &table->cells[table->rows - 1];
95} 117}
diff --git a/src/widget/widget.c b/src/widget/widget.c
index ef79ac4..ebcaf10 100644
--- a/src/widget/widget.c
+++ b/src/widget/widget.c
@@ -63,11 +63,28 @@ uiPtr uiMakeTablePtr(uiTable* table) {
63 return (uiPtr){.type = uiTypeTable, .table = table}; 63 return (uiPtr){.type = uiTypeTable, .table = table};
64} 64}
65 65
66static uiPtr uiMakeWidgetPtr(uiWidget* widget) { 66uiPtr uiMakeWidgetPtr(uiWidget* widget) {
67 assert(widget); 67 assert(widget);
68 return (uiPtr){.type = widget->type, .widget = widget}; 68 switch (widget->type) {
69 case uiTypeButton:
70 return uiMakeButtonPtr((uiButton*)widget);
71 case uiTypeFrame:
72 return uiMakeFramePtr((uiFrame*)widget);
73 case uiTypeLabel:
74 return uiMakeLabelPtr((uiLabel*)widget);
75 case uiTypeTable:
76 return uiMakeTablePtr((uiTable*)widget);
77 default:
78 ASSERT(false);
79 break;
80 }
81 return (uiPtr){0};
69} 82}
70 83
84uiPtr uiNullptr(void) { return (uiPtr){0}; }
85
86bool uiIsNullptr(uiPtr ptr) { return ptr.widget == 0; }
87
71uiButton* uiGetButtonPtr(uiPtr ptr) { 88uiButton* uiGetButtonPtr(uiPtr ptr) {
72 assert(ptr.type == uiTypeButton); 89 assert(ptr.type == uiTypeButton);
73 assert(ptr.button); 90 assert(ptr.button);
diff --git a/src/widget/widget.h b/src/widget/widget.h
index 63f3d94..db11164 100644
--- a/src/widget/widget.h
+++ b/src/widget/widget.h
@@ -16,39 +16,42 @@ typedef struct uiWidget {
16 Widget_list children; 16 Widget_list children;
17} uiWidget; 17} uiWidget;
18 18
19/// Button.
20typedef struct uiButton { 19typedef struct uiButton {
21 uiWidget widget; 20 uiWidget widget;
22 string text; 21 string text;
23} uiButton; 22} uiButton;
24 23
25/// Frame.
26typedef struct uiFrame { 24typedef struct uiFrame {
27 uiWidget widget; 25 uiWidget widget;
28} uiFrame; 26} uiFrame;
29 27
30/// Label.
31typedef struct uiLabel { 28typedef struct uiLabel {
32 uiWidget widget; 29 uiWidget widget;
33 string text; 30 string text;
34} uiLabel; 31} uiLabel;
35 32
36/// Table cell. 33typedef struct uiScrollbar {
34 int width;
35 int height; // Total height: handle plus scrollable area.
36 int handle_height; // Height of the scroll handle.
37 int handle_y; // Starting y-coordinate of the handle.
38} uiScrollbar;
39
37typedef struct uiCell { 40typedef struct uiCell {
38 string text; 41 string text;
39} uiCell; 42} uiCell;
40 43
41/// Table.
42typedef struct uiTable { 44typedef struct uiTable {
43 uiWidget widget; 45 uiWidget widget;
44 int rows; 46 int rows;
45 int cols; 47 int cols;
46 int height; // Height in pixels. 48 int height; // Height in pixels.
47 int* widths; // Width, in pixels, for each column. 49 int* widths; // Width, in pixels, for each column.
48 uiCell* header; // If non-null, row of 'cols' header cells. 50 uiCell* header; // If non-null, row of 'cols' header cells.
49 uiCell** cells; // Array of 'rows' rows, each of 'cols' cells. 51 uiCell** cells; // Array of 'rows' rows, each of 'cols' cells.
50 int offset; // Offset into the rows of the table. Units: rows. 52 int offset; // Offset into the rows of the table. Units: rows.
51 int num_visible_rows; // The number of rows that are visible at once. 53 int num_visible_rows; // The number of rows that are visible at once.
54 uiScrollbar scrollbar;
52 struct { 55 struct {
53 bool vertical_overflow : 1; // True if contents overflow vertically. 56 bool vertical_overflow : 1; // True if contents overflow vertically.
54 } flags; 57 } flags;
@@ -56,14 +59,24 @@ typedef struct uiTable {
56 59
57void DestroyWidget(uiWidget** ppWidget); 60void DestroyWidget(uiWidget** ppWidget);
58 61
59const uiCell* GetCell(const uiTable* table, int row, int col); 62/// Set the scrollbar handle's y-coordinate, which is clipped to the scrollbar's
60uiCell* GetCellMut(uiTable* table, int row, int col); 63/// rectangle.
61uiCell** GetLastRow(uiTable* table); 64///
65/// Return the handle's y-coordinate after clipping.
66int ScrollbarScroll(uiScrollbar*, int y);
62 67
63#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE)) 68const uiCell* TableGetCell(const uiTable*, int row, int col);
69uiCell* TableGetCellMut(uiTable*, int row, int col);
70uiCell** TableGetLastRow(uiTable*);
71void SyncScrollbarToTable(uiTable*);
72void SyncTableToScrollbar(uiTable*);
73
74static inline int Min(int a, int b) { return a < b ? a : b; }
75static inline int Max(int a, int b) { return a > b ? a : b; }
64 76
65static inline void* uiAlloc(size_t count, size_t size) { 77static inline void* uiAlloc(size_t count, size_t size) {
66 void* mem = calloc(count, size); 78 void* mem = calloc(count, size);
67 ASSERT(mem); 79 ASSERT(mem);
68 return mem; 80 return mem;
69} 81}
82#define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE))