diff options
| author | 3gg <3gg@shellblade.net> | 2026-03-12 15:29:23 -0700 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2026-03-12 15:29:23 -0700 |
| commit | 92978a10576d52a0f6c9983d3b6afae7c40eff40 (patch) | |
| tree | bf73faed8aa1ecd71b9f61c37a549faf4cd30372 | |
| parent | 58c0f40df5947b3933bf7b6564b2ba5dc39fbd92 (diff) | |
Support scrolling by dragging scrollbars
| -rw-r--r-- | CMakeLists.txt | 1 | ||||
| -rw-r--r-- | include/ui.h | 15 | ||||
| -rw-r--r-- | src/constants.h | 2 | ||||
| -rw-r--r-- | src/input.c | 226 | ||||
| -rw-r--r-- | src/layout.c | 21 | ||||
| -rw-r--r-- | src/render.c | 13 | ||||
| -rw-r--r-- | src/uiLibrary.h | 23 | ||||
| -rw-r--r-- | src/widget/scrollbar.c | 8 | ||||
| -rw-r--r-- | src/widget/table.c | 58 | ||||
| -rw-r--r-- | src/widget/widget.c | 21 | ||||
| -rw-r--r-- | src/widget/widget.h | 49 |
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. |
| 83 | typedef struct uiMouseButtonEvent { | 83 | typedef 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. | ||
| 102 | typedef 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. |
| 102 | typedef enum uiInputEventType { | 109 | typedef 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*); | |||
| 149 | uiPtr uiMakeFramePtr(uiFrame*); | 158 | uiPtr uiMakeFramePtr(uiFrame*); |
| 150 | uiPtr uiMakeLabelPtr(uiLabel*); | 159 | uiPtr uiMakeLabelPtr(uiLabel*); |
| 151 | uiPtr uiMakeTablePtr(uiTable*); | 160 | uiPtr uiMakeTablePtr(uiTable*); |
| 161 | uiPtr uiMakeWidgetPtr(uiWidget*); | ||
| 162 | uiPtr uiNullptr(void); | ||
| 163 | bool uiIsNullptr(uiPtr ptr); | ||
| 152 | 164 | ||
| 153 | uiButton* uiGetButtonPtr(uiPtr ptr); | 165 | uiButton* uiGetButtonPtr(uiPtr ptr); |
| 154 | uiFrame* uiGetFramePtr(uiPtr ptr); | 166 | uiFrame* uiGetFramePtr(uiPtr ptr); |
| @@ -188,6 +200,7 @@ void uiTableClear(uiTable*); | |||
| 188 | void uiTableAddRow(uiTable*, const char** row); | 200 | void uiTableAddRow(uiTable*, const char** row); |
| 189 | void uiTableSet(uiTable*, int row, int col, const char* text); | 201 | void uiTableSet(uiTable*, int row, int col, const char* text); |
| 190 | const char* uiTableGet(const uiTable*, int row, int col); | 202 | const char* uiTableGet(const uiTable*, int row, int col); |
| 203 | void 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. |
| 13 | static bool RectContains(uiRect rect, uiPoint point) { | 10 | static 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. | ||
| 39 | static 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. | ||
| 61 | static 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. | ||
| 39 | static void GetTableRowColAtXy( | 76 | static 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. | ||
| 114 | static 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) { | |||
| 86 | static void ScrollTable(uiTable* table, const uiMouseScrollEvent* event) { | 152 | static 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. |
| 95 | static bool ProcessScrollEvent( | 161 | static 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. | ||
| 172 | static 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. |
| 115 | static bool ProcessClickEvent( | 192 | static 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. | ||
| 212 | static 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. | ||
| 232 | static 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 | |||
| 134 | bool uiSendEvent(uiFrame* frame, const uiInputEvent* event) { | 251 | bool 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. | ||
| 17 | typedef 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. | ||
| 25 | typedef 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 | |||
| 9 | typedef struct uiLibrary { | 30 | typedef 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 | |||
| 3 | int 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 | ||
| 3 | const 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 | |||
| 8 | uiCell* GetCellMut(uiTable* table, int row, int col) { | ||
| 9 | assert(table); | ||
| 10 | return (uiCell*)GetCell(table, row, col); | ||
| 11 | } | ||
| 12 | |||
| 13 | uiCell** GetLastRow(uiTable* table) { | ||
| 14 | assert(table); | ||
| 15 | assert(table->rows > 0); | ||
| 16 | return &table->cells[table->rows - 1]; | ||
| 17 | } | ||
| 18 | 5 | ||
| 19 | uiTable* uiMakeTable(int rows, int cols, const char** header) { | 6 | uiTable* 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) { | |||
| 86 | void uiTableSet(uiTable* table, int row, int col, const char* text) { | 73 | void 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 | ||
| 92 | const char* uiTableGet(const uiTable* table, int row, int col) { | 79 | const 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 | |||
| 84 | void 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 | |||
| 90 | void SyncScrollbarToTable(uiTable* table) { | ||
| 91 | assert(table); | ||
| 92 | ScrollbarScroll( | ||
| 93 | &table->scrollbar, (int)((double)table->offset / (double)table->rows * | ||
| 94 | (double)table->height)); | ||
| 95 | } | ||
| 96 | |||
| 97 | void SyncTableToScrollbar(uiTable* table) { | ||
| 98 | assert(table); | ||
| 99 | table->offset = (int)((double)table->scrollbar.handle_y / | ||
| 100 | (double)table->height * (double)table->rows); | ||
| 101 | } | ||
| 102 | |||
| 103 | const uiCell* TableGetCell(const uiTable* table, int row, int col) { | ||
| 104 | assert(table); | ||
| 105 | return &table->cells[row][col]; | ||
| 106 | } | ||
| 107 | |||
| 108 | uiCell* TableGetCellMut(uiTable* table, int row, int col) { | ||
| 109 | assert(table); | ||
| 110 | return (uiCell*)TableGetCell(table, row, col); | ||
| 111 | } | ||
| 112 | |||
| 113 | uiCell** 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 | ||
| 66 | static uiPtr uiMakeWidgetPtr(uiWidget* widget) { | 66 | uiPtr 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 | ||
| 84 | uiPtr uiNullptr(void) { return (uiPtr){0}; } | ||
| 85 | |||
| 86 | bool uiIsNullptr(uiPtr ptr) { return ptr.widget == 0; } | ||
| 87 | |||
| 71 | uiButton* uiGetButtonPtr(uiPtr ptr) { | 88 | uiButton* 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. | ||
| 20 | typedef struct uiButton { | 19 | typedef struct uiButton { |
| 21 | uiWidget widget; | 20 | uiWidget widget; |
| 22 | string text; | 21 | string text; |
| 23 | } uiButton; | 22 | } uiButton; |
| 24 | 23 | ||
| 25 | /// Frame. | ||
| 26 | typedef struct uiFrame { | 24 | typedef struct uiFrame { |
| 27 | uiWidget widget; | 25 | uiWidget widget; |
| 28 | } uiFrame; | 26 | } uiFrame; |
| 29 | 27 | ||
| 30 | /// Label. | ||
| 31 | typedef struct uiLabel { | 28 | typedef struct uiLabel { |
| 32 | uiWidget widget; | 29 | uiWidget widget; |
| 33 | string text; | 30 | string text; |
| 34 | } uiLabel; | 31 | } uiLabel; |
| 35 | 32 | ||
| 36 | /// Table cell. | 33 | typedef 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 | |||
| 37 | typedef struct uiCell { | 40 | typedef struct uiCell { |
| 38 | string text; | 41 | string text; |
| 39 | } uiCell; | 42 | } uiCell; |
| 40 | 43 | ||
| 41 | /// Table. | ||
| 42 | typedef struct uiTable { | 44 | typedef 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 | ||
| 57 | void DestroyWidget(uiWidget** ppWidget); | 60 | void DestroyWidget(uiWidget** ppWidget); |
| 58 | 61 | ||
| 59 | const uiCell* GetCell(const uiTable* table, int row, int col); | 62 | /// Set the scrollbar handle's y-coordinate, which is clipped to the scrollbar's |
| 60 | uiCell* GetCellMut(uiTable* table, int row, int col); | 63 | /// rectangle. |
| 61 | uiCell** GetLastRow(uiTable* table); | 64 | /// |
| 65 | /// Return the handle's y-coordinate after clipping. | ||
| 66 | int ScrollbarScroll(uiScrollbar*, int y); | ||
| 62 | 67 | ||
| 63 | #define UI_NEW(TYPE) (TYPE*)uiAlloc(1, sizeof(TYPE)) | 68 | const uiCell* TableGetCell(const uiTable*, int row, int col); |
| 69 | uiCell* TableGetCellMut(uiTable*, int row, int col); | ||
| 70 | uiCell** TableGetLastRow(uiTable*); | ||
| 71 | void SyncScrollbarToTable(uiTable*); | ||
| 72 | void SyncTableToScrollbar(uiTable*); | ||
| 73 | |||
| 74 | static inline int Min(int a, int b) { return a < b ? a : b; } | ||
| 75 | static inline int Max(int a, int b) { return a > b ? a : b; } | ||
| 64 | 76 | ||
| 65 | static inline void* uiAlloc(size_t count, size_t size) { | 77 | static 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)) | ||
